From c1c46f1e4eb22aaa08c4a9567fafe905db5a2284 Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Mon, 17 Aug 2026 13:42:51 -0700 Subject: [PATCH] feat: Claude login on the new-agent page before agent creation (#11347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The Claude local adapter supports subscription login through a sandbox > - The new-agent page must show login before the user creates an agent > - Test results must not expose raw sandbox diagnostics or secret values > - This pull request adds the login UI to both Test lanes and closes the diagnostic boundary > - The branch also adds durable cleanup recovery for failed sandbox teardown > - Reusable sandboxes must retain both their recorded teardown configuration and a valid lifecycle path until destruction succeeds > - The benefit is a usable login flow with fixed public checks, redacted server logs, and recoverable sandbox cleanup ## Linked Issues or Issue Description Related public work: [#9488](https://github.com/paperclipai/paperclip/pull/9488) adds first-class recognition for `CLAUDE_CODE_OAUTH_TOKEN` in headless and remote runs. Related public issue: [#2681](https://github.com/paperclipai/paperclip/issues/2681) requests Claude Code subscription support. This pull request adds the login transport and new-agent UI flow that those changes do not provide. **Subsystem affected:** Claude local adapter, server login probes, sandbox provider setup, cleanup recovery, and the new-agent UI. **Problem or motivation:** The Test lanes did not show the sandbox login panel in all supported cases. Test results also exposed raw probe diagnostics, and JSON escapes could end secret redaction early. **Proposed solution:** Surface the login capability through the bundled provider manifest. Prepare the same probe runtime in the ACP lane. Send diagnostics only to redacted server logs. Keep Test checks on fixed public messages. Normalize login URL hints to allowlisted HTTPS Claude and Anthropic hosts. Consume JSON escapes during redaction. Preserve failed sandbox cleanup state across retries and restarts, and prevent deletion from severing the lifecycle context of a live reusable sandbox. **Alternatives considered:** Keep raw diagnostics in Test checks or trust login URL text from the sandbox. Both choices increase information exposure. Keep separate probe behavior in the ACP lane. That choice would leave the two Test lanes inconsistent. ## What Changed - Surface the sandbox login panel on both Test lanes. - Reconcile the bundled Daytona plugin manifest so `supportsSetupTokenLogin` reaches the UI capability gate. - Prepare the ACP Test lane with the same probe runtime as the CLI Test lane. - Add the `claude_acp_login_probe_unavailable` warning when the ACP probe cannot run. - Send raw sandbox diagnostics only to redacted server logs. - Keep Test checks on fixed public messages in the ACP, managed-config, and CLI paths. - Normalize login URL hints to allowlisted HTTPS Claude and Anthropic hosts. - Redact JSON and escaped-JSON secret values, including escaped quotes and backslashes. - Preserve orphan cleanup records across provider failures, restarts, and unavailable plugins. - Atomically block environment deletion while a live reusable sandbox lease still depends on it. - Verify pending cleanup destroys plugin sandboxes with the provider configuration recorded on the lease, even after the current environment configuration changes. ## Verification - Head under review: `506b7fa2d83c36bfa5fd722ee9d95b0c7431c241`. - Focused environment route/service/runtime coverage passes: 196 tests across 3 files. - `pnpm -r typecheck` passes. - `pnpm build` passes. - The full Vitest run completed with 4,754 passing and 28 failing tests. All 23 source-test failures reproduce unchanged on parent head `58cfe61a33191ce03d965d65085d26064b4888ba`; the other 5 are duplicate executions from stale `server/dist` output. The failures are unrelated macOS path/listener and scheduler-fixture failures, so there is no new bad commit for bisect to localize. - All required CI checks pass for the current head, including build, typecheck/release registry, all server and workspace shards, serialized server suites, canary, and e2e. - A fresh Greptile review for `506b7fa2d83c36bfa5fd722ee9d95b0c7431c241` reports 5/5, “safe to merge,” with no blocking failure remaining. ## Risks - A probe or redaction change could hide useful server diagnostics. - An allowlist change could reject a valid Claude login URL. - Cleanup recovery changes could affect provider teardown ordering. - An environment with a live reusable sandbox can no longer be deleted until the owning issue or execution workspace completes teardown. - The implementation keeps public Test messages fixed and sends detail to redacted server logs. ## Model Used OpenAI GPT-5 via Codex — exact model ID: GPT-5; tool use and code execution enabled; extended reasoning enabled. The implementation author used AI-assisted development. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes: #` / `Refs: #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and documented the result - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation or confirmed no separate documentation change is needed - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- .../src/command-redaction.test.ts | 81 + .../adapter-utils/src/command-redaction.ts | 34 + packages/adapter-utils/src/index.ts | 1 + packages/adapter-utils/src/types.ts | 15 + .../src/server/__fixtures__/setup-token.md | 93 +- .../claude-local/src/server/acp.auth.test.ts | 365 + .../adapters/claude-local/src/server/acp.ts | 210 +- .../claude-local/src/server/auth-check.ts | 12 + .../claude-local/src/server/claude-config.ts | 131 +- .../adapters/claude-local/src/server/index.ts | 5 +- .../claude-local/src/server/parse.test.ts | 112 + .../adapters/claude-local/src/server/parse.ts | 63 +- .../src/server/probe-diagnostics.test.ts | 109 + .../src/server/probe-diagnostics.ts | 87 + .../src/server/probe-redaction.test.ts | 97 + .../setup-token-characterization.test.ts | 145 + .../src/server/setup-token-parse.test.ts | 497 +- .../src/server/setup-token-parse.ts | 414 +- .../src/server/setup-token-runner.test.ts | 86 +- .../src/server/setup-token-runner.ts | 111 +- .../src/server/test.probe.test.ts | 255 +- .../src/server/test.remote.test.ts | 139 + .../adapters/claude-local/src/server/test.ts | 158 +- ...claude-setup-token-sessions-schema.test.ts | 116 + .../migrations/0221_swift_justin_hammer.sql | 20 + ..._cleanup_env_reference_survives_delete.sql | 4 + .../db/src/migrations/meta/0221_snapshot.json | 39714 ++++++++++++++++ .../db/src/migrations/meta/0222_snapshot.json | 39714 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 14 + .../src/schema/claude_setup_token_sessions.ts | 83 + packages/db/src/schema/environment_leases.ts | 9 +- packages/db/src/schema/index.ts | 5 + .../sandbox-providers/daytona/src/manifest.ts | 9 +- .../daytona/src/plugin.test.ts | 70 + .../sandbox-providers/daytona/src/plugin.ts | 159 +- packages/plugins/sdk/src/define-plugin.ts | 32 + packages/plugins/sdk/src/index.ts | 2 + packages/plugins/sdk/src/protocol.ts | 140 + packages/plugins/sdk/src/testing.ts | 8 + packages/plugins/sdk/src/types.ts | 34 + packages/plugins/sdk/src/worker-rpc-host.ts | 74 + .../plugins/sdk/tests/worker-rpc-host.test.ts | 150 + packages/shared/src/environment-support.ts | 3 + packages/shared/src/index.ts | 31 + packages/shared/src/types/agent.ts | 89 + packages/shared/src/types/environment.ts | 24 +- packages/shared/src/types/index.ts | 10 + packages/shared/src/types/plugin.ts | 7 + packages/shared/src/validators/agent.ts | 9 + .../claude-setup-token-session.test.ts | 141 + .../validators/claude-setup-token-session.ts | 170 + packages/shared/src/validators/plugin.ts | 1 + .../agent-adapter-validation-routes.test.ts | 52 + .../agent-device-login-routes.test.ts | 3 + .../agent-permissions-routes.test.ts | 8 + .../src/__tests__/agent-skills-routes.test.ts | 3 + .../agents-claude-oauth-binding.test.ts | 948 + server/src/__tests__/bundled-plugins.test.ts | 110 +- .../src/__tests__/environment-routes.test.ts | 160 + .../src/__tests__/environment-runtime.test.ts | 2485 +- .../src/__tests__/environment-service.test.ts | 511 + .../plugin-worker-setup-token-pty.cjs | 161 + .../heartbeat-pending-cleanup-sweep.test.ts | 407 +- server/src/__tests__/openapi-routes.test.ts | 80 +- .../__tests__/plugin-worker-manager.test.ts | 260 + .../sandbox-orphan-cleanup-spool.test.ts | 93 + ...ecrets-routes-claude-oauth-service.test.ts | 610 + ...crets-service-dangling-user-secret.test.ts | 190 + .../server-startup-feedback-export.test.ts | 7 +- server/src/app.ts | 107 +- server/src/index.ts | 82 +- server/src/routes/agents.ts | 676 +- server/src/routes/environments.ts | 28 + server/src/routes/openapi.ts | 149 +- server/src/routes/setup-token-route.test.ts | 865 +- server/src/services/agents.ts | 161 +- server/src/services/bundled-plugins.ts | 63 +- server/src/services/environment-config.ts | 39 + server/src/services/environment-runtime.ts | 891 +- server/src/services/environments.ts | 269 +- server/src/services/heartbeat.ts | 129 +- .../src/services/plugin-environment-driver.ts | 2 + server/src/services/plugin-worker-manager.ts | 386 + .../services/sandbox-orphan-cleanup-spool.ts | 198 + .../src/services/sandbox-provider-runtime.ts | 16 + server/src/services/secrets.ts | 563 +- .../src/services/setup-token-session.test.ts | 843 +- server/src/services/setup-token-session.ts | 827 +- .../setup-token-transport-binding.test.ts | 892 + .../services/setup-token-transport-binding.ts | 611 + server/src/shutdown.test.ts | 131 + server/src/shutdown.ts | 50 + ui/src/api/agents.ts | 52 + .../AgentConfigForm.render.test.tsx | 1331 +- ui/src/components/AgentConfigForm.tsx | 746 +- .../environment-variables-editor/model.ts | 27 + ui/src/lib/new-agent-hire-payload.test.ts | 26 + ui/src/lib/new-agent-hire-payload.ts | 10 + ui/src/pages/Agents.test.tsx | 2 + ui/src/pages/NewAgent.test.tsx | 380 + ui/src/pages/NewAgent.tsx | 37 + 101 files changed, 99708 insertions(+), 1031 deletions(-) create mode 100644 packages/adapter-utils/src/command-redaction.test.ts create mode 100644 packages/adapters/claude-local/src/server/acp.auth.test.ts create mode 100644 packages/adapters/claude-local/src/server/auth-check.ts create mode 100644 packages/adapters/claude-local/src/server/probe-diagnostics.test.ts create mode 100644 packages/adapters/claude-local/src/server/probe-diagnostics.ts create mode 100644 packages/adapters/claude-local/src/server/probe-redaction.test.ts create mode 100644 packages/adapters/claude-local/src/server/setup-token-characterization.test.ts create mode 100644 packages/adapters/claude-local/src/server/test.remote.test.ts create mode 100644 packages/db/src/claude-setup-token-sessions-schema.test.ts create mode 100644 packages/db/src/migrations/0221_swift_justin_hammer.sql create mode 100644 packages/db/src/migrations/0222_orphan_cleanup_env_reference_survives_delete.sql create mode 100644 packages/db/src/migrations/meta/0221_snapshot.json create mode 100644 packages/db/src/migrations/meta/0222_snapshot.json create mode 100644 packages/db/src/schema/claude_setup_token_sessions.ts create mode 100644 packages/shared/src/validators/claude-setup-token-session.test.ts create mode 100644 packages/shared/src/validators/claude-setup-token-session.ts create mode 100644 server/src/__tests__/agents-claude-oauth-binding.test.ts create mode 100644 server/src/__tests__/fixtures/plugin-worker-setup-token-pty.cjs create mode 100644 server/src/__tests__/sandbox-orphan-cleanup-spool.test.ts create mode 100644 server/src/__tests__/secrets-routes-claude-oauth-service.test.ts create mode 100644 server/src/__tests__/secrets-service-dangling-user-secret.test.ts create mode 100644 server/src/services/sandbox-orphan-cleanup-spool.ts create mode 100644 server/src/services/setup-token-transport-binding.test.ts create mode 100644 server/src/services/setup-token-transport-binding.ts create mode 100644 ui/src/pages/NewAgent.test.tsx diff --git a/packages/adapter-utils/src/command-redaction.test.ts b/packages/adapter-utils/src/command-redaction.test.ts new file mode 100644 index 0000000000..cbbfc8ce19 --- /dev/null +++ b/packages/adapter-utils/src/command-redaction.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest"; +import { REDACTED_COMMAND_TEXT_VALUE, redactDiagnosticText } from "./command-redaction.js"; + +describe("redactDiagnosticText", () => { + it("redacts a JSON secret field value", () => { + const input = '{"token":"opaque-value","status":"error"}'; + const output = redactDiagnosticText(input); + expect(output).not.toContain("opaque-value"); + expect(output).toContain(`"token":"${REDACTED_COMMAND_TEXT_VALUE}"`); + // The non-secret field keeps its value. + expect(output).toContain('"status":"error"'); + }); + + it("redacts an api_key JSON field with whitespace around the colon", () => { + const input = '{ "api_key" : "sk-secret-123" }'; + const output = redactDiagnosticText(input); + expect(output).not.toContain("sk-secret-123"); + expect(output).toContain(REDACTED_COMMAND_TEXT_VALUE); + }); + + it("redacts an escaped-JSON secret field value", () => { + // A diagnostic can carry a JSON string, so the double quotes appear as `\"`. + const input = '{\\"token\\":\\"opaque-value\\"}'; + const output = redactDiagnosticText(input); + expect(output).not.toContain("opaque-value"); + expect(output).toContain(`\\"token\\":\\"${REDACTED_COMMAND_TEXT_VALUE}\\"`); + }); + + it("still redacts a shell KEY=value secret", () => { + const input = "ANTHROPIC_API_KEY=super-secret-value claude --print"; + const output = redactDiagnosticText(input); + expect(output).not.toContain("super-secret-value"); + expect(output).toContain(REDACTED_COMMAND_TEXT_VALUE); + }); + + it("keeps non-secret text and non-secret JSON fields intact", () => { + const input = '{"status":"ok","message":"probe finished"}'; + expect(redactDiagnosticText(input)).toBe(input); + }); + + it("redacts the secret but keeps a non-secret marker in the same string", () => { + const input = 'DIAGMARKER1234 said {"authorization":"Bearer opaque"}'; + const output = redactDiagnosticText(input); + expect(output).toContain("DIAGMARKER1234"); + expect(output).not.toContain("opaque"); + }); + + it("redacts a JSON secret value that contains an escaped quote", () => { + // The value holds an escaped quote, so a naive matcher stops at the `\"` and + // leaves the rest of the credential. The marker sits after the escaped quote. + const input = '{"token":"pre\\"MARKERQUOTE_A"}'; + const output = redactDiagnosticText(input); + expect(output).not.toContain("MARKERQUOTE_A"); + expect(output).toContain(`"token":"${REDACTED_COMMAND_TEXT_VALUE}"`); + }); + + it("redacts a JSON secret value that contains an escaped backslash", () => { + const input = '{"secret":"pre\\\\MARKERBACKSLASH_A"}'; + const output = redactDiagnosticText(input); + expect(output).not.toContain("MARKERBACKSLASH_A"); + expect(output).toContain(`"secret":"${REDACTED_COMMAND_TEXT_VALUE}"`); + }); + + it("redacts an escaped-JSON secret value that contains an escaped quote", () => { + // A diagnostic can carry a serialized JSON string, so the whole JSON is + // escaped a second time. The inner value still holds an escaped quote. + const innerJson = '{"token":"pre\\"MARKERQUOTE_B"}'; + const input = JSON.stringify(innerJson); + const output = redactDiagnosticText(input); + expect(output).not.toContain("MARKERQUOTE_B"); + expect(output).toContain(REDACTED_COMMAND_TEXT_VALUE); + }); + + it("redacts an escaped-JSON secret value that contains an escaped backslash", () => { + const innerJson = '{"password":"pre\\\\MARKERBACKSLASH_B"}'; + const input = JSON.stringify(innerJson); + const output = redactDiagnosticText(input); + expect(output).not.toContain("MARKERBACKSLASH_B"); + expect(output).toContain(REDACTED_COMMAND_TEXT_VALUE); + }); +}); diff --git a/packages/adapter-utils/src/command-redaction.ts b/packages/adapter-utils/src/command-redaction.ts index 86024755aa..3ee2756edd 100644 --- a/packages/adapter-utils/src/command-redaction.ts +++ b/packages/adapter-utils/src/command-redaction.ts @@ -56,3 +56,37 @@ export function redactCommandText(command: string, redactedValue = REDACTED_COMM .replace(COMMAND_GITHUB_TOKEN_RE, redactedValue) .replace(COMMAND_JWT_RE, redactedValue); } + +// A JSON secret field is a key/value pair such as `"token":"opaque-value"`. The +// command redaction handles shell `KEY=value` syntax only. A sandbox diagnostic +// can also carry a serialized JSON error, so the sanitizer must redact the JSON +// form too. The value body consumes JSON escape sequences. An escaped quote +// (`\"`) inside the value does not end the match early. +const JSON_SECRET_FIELD_RE = new RegExp( + String.raw`("(?:${SECRET_NAME_PATTERN})"\s*:\s*")(?:\\[\s\S]|[^"\\])*(")`, + "gi", +); +// An escaped JSON secret field is the same pair inside a JSON string. The double +// quote appears as `\"` and a backslash appears as `\\`. The value body +// consumes the doubled escape sequences. An escaped quote inside the value does +// not end the match early. The value ends at the next unescaped `\"`. +const JSON_ESCAPED_SECRET_FIELD_RE = new RegExp( + String.raw`(\\"(?:${SECRET_NAME_PATTERN})\\"\s*:\s*\\")(?:\\\\\\\\|\\\\\\"|\\\\[\s\S]|[^\\"])*(\\")`, + "gi", +); + +/** + * Redact secrets from an untrusted diagnostic string. + * + * The function first runs the command redaction. The command redaction handles + * shell `KEY=value` assignments, CLI secret options, bearer headers, and common + * token shapes. The function then redacts JSON and escaped-JSON secret fields, + * because a sandbox diagnostic can carry a serialized JSON error such as + * `{"token":"opaque-value"}`. The caller must still bound the length after this + * step. + */ +export function redactDiagnosticText(text: string, redactedValue = REDACTED_COMMAND_TEXT_VALUE): string { + return redactCommandText(text, redactedValue) + .replace(JSON_ESCAPED_SECRET_FIELD_RE, `$1${redactedValue}$2`) + .replace(JSON_SECRET_FIELD_RE, `$1${redactedValue}$2`); +} diff --git a/packages/adapter-utils/src/index.ts b/packages/adapter-utils/src/index.ts index 5df19260aa..ebe6409e70 100644 --- a/packages/adapter-utils/src/index.ts +++ b/packages/adapter-utils/src/index.ts @@ -63,6 +63,7 @@ export { export { REDACTED_COMMAND_TEXT_VALUE, redactCommandText, + redactDiagnosticText, } from "./command-redaction.js"; export { buildSandboxNpmInstallCommand } from "./sandbox-install-command.js"; export { diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index b6d7439a5d..3957bbbce3 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -579,6 +579,21 @@ export interface CreateConfigValues { envBindings: Record; url: string; bootstrapPrompt: string; + /** + * The non-secret stored-session claim from a completed Claude subscription + * login. The create form holds it after the login reaches the server `stored` + * state and sends it in the agent create request. The server consumes the + * claim to bind the fixed `CLAUDE_CODE_OAUTH_TOKEN`. It never carries a token. + */ + claudeStoredSessionId?: string | null; + /** + * True when the create form binds the fixed `CLAUDE_CODE_OAUTH_TOKEN` + * reference to an existing stored owner login with no new login round trip. + * The form sets it after the owner clicks apply-existing. The server binds the + * fixed reference only for a user actor and only when a stored value exists. It + * never carries a token. + */ + claudeApplyStoredLogin?: boolean; payloadTemplateJson?: string; workspaceStrategyType?: string; workspaceBaseRef?: string; diff --git a/packages/adapters/claude-local/src/server/__fixtures__/setup-token.md b/packages/adapters/claude-local/src/server/__fixtures__/setup-token.md index 88d2f0020b..5fe80619e4 100644 --- a/packages/adapters/claude-local/src/server/__fixtures__/setup-token.md +++ b/packages/adapters/claude-local/src/server/__fixtures__/setup-token.md @@ -61,4 +61,95 @@ No real login or token was captured in this phase. Implementation should provisi ## Smallest proof -`claude --version`; one pipe harness with a synthetic invalid code; and one PTY harness with bracketed-paste input showed the stream split, redacted URL structure, masked echo, HTTP 400 retry behavior, and controlled terminal exits. A literal scan of this document confirms it contains no authorization query values, browser code, or live token. \ No newline at end of file +`claude --version`; one pipe harness with a synthetic invalid code; and one PTY harness with bracketed-paste input showed the stream split, redacted URL structure, masked echo, HTTP 400 retry behavior, and controlled terminal exits. A literal scan of this document confirms it contains no authorization query values, browser code, or live token. +## Re-characterization on Claude Code 2.1.226 (2026-08-13) + +- Re-characterized: 2026-08-13 +- Claude Code: `2.1.226` +- Environment: real pseudo-terminal, headless, no browser. Prompt phase only; no + login was completed and no token was minted. +- Safety: no real authorization URL, browser code, or token is recorded here. + +A live run of `claude setup-token` on a real pseudo-terminal confirmed the prompt +contract is unchanged in substance from `2.1.205`: + +- URL preamble line: `Browser didn't open? Use the url below to sign in (c to copy)`. +- Authorization URL origin `https://claude.com`, path `/cai/oauth/authorize`, the + same eight query keys (`client_id`, `code`, `code_challenge`, + `code_challenge_method`, `redirect_uri`, `response_type`, `scope`, `state`), no + fragment. The `redirect_uri` value host is `https://platform.claude.com`. +- Browser-code prompt line: `Paste code here if prompted >`. + +The live run recorded two terminal-rendering behaviors that the earlier +normalized capture hid. The parser now handles both: + +1. Word spacing. The login UI lays out each word at an absolute column with a + Cursor Horizontal Absolute (`ESC[G`) sequence, so it emits no literal space + between two words. A parser that removes the control sequence glues the words + together. The parser now renders each horizontal-move sequence as one space. +2. Repeated URL. The UI emits one OSC 8 hyperlink per wrapped display row, so it + repeats the full authorization URL on a few consecutive lines before the + prompt. The parser now skips a repeated URL line and binds the prompt on the + first non-blank line after the URL block; any other unrelated line still fails + the bind. + +The success anchors and the token shape were not re-run live, because a real +login needs a subscription and an out-of-band browser. They stay as recorded in +[`setup-token-success.md`](./setup-token-success.md): the before-anchor line +`Your OAuth token (valid for 1 year):`, the after-anchor line `Store this token +securely. You won't be able to see it again.`, and the token prefix +`sk-ant-oat01-`. The success screen renders its anchor words with the same +Cursor Horizontal Absolute spacing, so the same rendering fix applies to the +token parser. + +An executable characterization test drives this live run. See +[`../setup-token-characterization.test.ts`](../setup-token-characterization.test.ts); +it is opt-in through `RUN_CLAUDE_SETUP_TOKEN_CHARACTERIZATION=1`. + +## Re-characterization on Claude Code 2.1.19 (2026-08-15) + +- Re-characterized: 2026-08-15 +- Claude Code: `2.1.19` +- Environment: real Daytona sandbox, image `daytonaio/sandbox:0.8.0`, headless, no + browser. Prompt phase only; no login was completed and no token was minted. +- Safety: no real authorization URL, browser code, or token is recorded here. + Every query value below is a same-shape synthetic placeholder that passes the + parser value validators. + +The production sandbox image ships `claude` 2.1.19. A real Daytona smoke found +that this version emits a different login contract than `2.1.205` and `2.1.226`. +The parser now accepts both contracts: + +- Authorization URL for 2.1.19: origin `https://claude.ai`, path + `/oauth/authorize`. The full pair is `https://claude.ai/oauth/authorize`. +- Authorization URL for 2.1.205 and 2.1.226: origin `https://claude.com`, path + `/cai/oauth/authorize`. The full pair is `https://claude.com/cai/oauth/authorize`. +- Both versions carry the same eight query keys: `client_id`, `code`, + `code_challenge`, `code_challenge_method`, `redirect_uri`, `response_type`, + `scope`, `state`. No fragment. +- The `redirect_uri` value is the static callback + `https://platform.claude.com/oauth/code/callback`. A real 2.1.19 capture + confirmed this exact value, so the parser keeps the single pin. +- The `code` value is a short opaque token. A real 2.1.19 capture measured a + four-character value, so the parser accepts a `code` value of one or more + characters. +- Browser-code prompt line: `Paste code here if prompted >`. + +Two rendering differences from `2.1.226`: + +1. No OSC 8 hyperlink. `2.1.19` prints the URL as plain text, not as an OSC 8 + hyperlink. +2. Hard line wrap. `2.1.19` wraps the plain-text URL across several physical + lines at the terminal width, with no space at the wrap. The parser joins the + URL-character-only physical lines and validates only the final reassembled + string. + +Sanitized rendered form of the wrapped URL (placeholder values): + +```text +Browser didn't open? Use the url below to sign in (c to copy) +https://claude.ai/oauth/authorize?client_id=9d1c8f00-1a2b-3c4d-5e6f-708192a3b4c5&code=wZ9x&code_challenge=E9Melhoa2OwvFr +EMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fplatform.claude.com%2Foauth%2Fcode%2 +Fcallback&response_type=code&scope=user%3Ainference&state=Xy7Kd2Pq9Rn4Vb8Lf1Mw6Zc3Hj0Tg5Us +Paste code here if prompted > +``` diff --git a/packages/adapters/claude-local/src/server/acp.auth.test.ts b/packages/adapters/claude-local/src/server/acp.auth.test.ts new file mode 100644 index 0000000000..a906234810 --- /dev/null +++ b/packages/adapters/claude-local/src/server/acp.auth.test.ts @@ -0,0 +1,365 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { AdapterExecutionResult } from "@paperclipai/adapter-utils"; +import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target"; + +// A shared handle so each test can set the stub Claude hello-probe output the +// sandbox runner returns. +const { runAdapterExecutionTargetProcess, probeResult } = vi.hoisted(() => { + const probeResult: { + value: { exitCode: number; stdout: string; stderr: string; timedOut: boolean }; + throwError: Error | null; + } = { + value: { exitCode: 1, stdout: "", stderr: "", timedOut: false }, + throwError: null, + }; + return { + probeResult, + runAdapterExecutionTargetProcess: vi.fn(async () => { + if (probeResult.throwError) throw probeResult.throwError; + return { + exitCode: probeResult.value.exitCode, + signal: null, + timedOut: probeResult.value.timedOut, + stdout: probeResult.value.stdout, + stderr: probeResult.value.stderr, + pid: 321, + startedAt: new Date().toISOString(), + }; + }), + }; +}); + +vi.mock("@paperclipai/adapter-utils/execution-target", async () => { + const actual = await vi.importActual( + "@paperclipai/adapter-utils/execution-target", + ); + return { + ...actual, + runAdapterExecutionTargetProcess, + }; +}); + +import { mapClaudeAcpAuthErrorCode, probeClaudeAcpSandboxLogin } from "./acp.js"; +import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js"; + +const sandboxTarget: AdapterExecutionTarget = { + kind: "remote", + transport: "sandbox", + providerKey: "daytona", + remoteCwd: "/home/daytona/paperclip-workspace", + runner: { + execute: async () => ({ + exitCode: 0, + signal: null, + timedOut: false, + stdout: "", + stderr: "", + pid: null, + startedAt: new Date().toISOString(), + }), + }, +}; + +const initLine = + '{"type":"system","subtype":"init","cwd":"/home/daytona/paperclip-workspace","session_id":"abc","tools":["Bash","Read"]}'; + +const loginRequiredStdout = [ + initLine, + '{"type":"result","subtype":"error_during_execution","is_error":true,"result":"Please run `claude login` to authenticate.","session_id":"abc"}', +].join("\n"); + +const helloStdout = [ + initLine, + '{"type":"result","subtype":"success","is_error":false,"result":"hello","session_id":"abc"}', +].join("\n"); + +// The real Claude CLI output for CLAUDE_CODE_OAUTH_TOKEN=invalid. The probe +// exits non-zero and the result event reports a 401 authentication failure with +// an "Invalid bearer token" message. A synthetic bearer marker rides along on a +// retry line, so the test can prove the raw probe text never reaches a check. +const invalidTokenMarker = "SUPERSECRETbearerMARKER"; +const invalidTokenStdout = [ + initLine, + `{"type":"system","subtype":"api_retry","attempt":1,"error_status":401,"error":"authentication_failed: bearer ${invalidTokenMarker} is invalid","session_id":"abc"}`, + '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Failed to authenticate. API Error: 401 Invalid bearer token"}]},"session_id":"abc","error":"authentication_failed"}', + '{"type":"result","subtype":"success","is_error":true,"api_error_status":401,"error":"authentication_failed","result":"Failed to authenticate. API Error: 401 Invalid bearer token","session_id":"abc"}', +].join("\n"); + +afterEach(() => { + vi.clearAllMocks(); + probeResult.value = { exitCode: 1, stdout: "", stderr: "", timedOut: false }; + probeResult.throwError = null; +}); + +describe("mapClaudeAcpAuthErrorCode", () => { + it("translates the generic acpx_auth_required code into claude_auth_required", () => { + const engineResult: AdapterExecutionResult = { + exitCode: 1, + signal: null, + timedOut: false, + errorMessage: "Claude requires login.", + errorCode: "acpx_auth_required", + errorMeta: { category: "auth", errorName: "Error" }, + }; + + const mapped = mapClaudeAcpAuthErrorCode(engineResult); + + // The user interface run gate reads claude_auth_required to show the login + // affordance on the default ACP path. + expect(mapped.errorCode).toBe("claude_auth_required"); + // The mapping keeps every other field so diagnostics stay intact. + expect(mapped.errorMessage).toBe("Claude requires login."); + expect(mapped.errorMeta).toEqual({ category: "auth", errorName: "Error" }); + }); + + it("leaves a different error code unchanged", () => { + const engineResult: AdapterExecutionResult = { + exitCode: 1, + signal: null, + timedOut: false, + errorCode: "acpx_runtime_error", + }; + + expect(mapClaudeAcpAuthErrorCode(engineResult).errorCode).toBe("acpx_runtime_error"); + }); + + it("leaves a null error code unchanged", () => { + const engineResult: AdapterExecutionResult = { + exitCode: 0, + signal: null, + timedOut: false, + errorCode: null, + }; + + expect(mapClaudeAcpAuthErrorCode(engineResult).errorCode).toBeNull(); + }); +}); + +describe("probeClaudeAcpSandboxLogin", () => { + it("emits the canonical adapter_auth_missing check when the sandbox probe reports missing auth", async () => { + probeResult.value = { exitCode: 1, stdout: loginRequiredStdout, stderr: "", timedOut: false }; + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + expect(checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(true); + // The descriptive check stays for diagnostics. + expect(checks.some((check) => check.code === "claude_hello_probe_auth_required")).toBe(true); + // A missing-auth probe is a warning, not a failure, so the environment stays + // testable and the user interface can offer login. + expect(checks.every((check) => check.level === "warn")).toBe(true); + }); + + it("classifies an invalid or expired token as adapter_auth_missing without leaking the token", async () => { + probeResult.value = { exitCode: 1, stdout: invalidTokenStdout, stderr: "", timedOut: false }; + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + // An auth failure returns the canonical login gate code, not the + // probe-could-not-run code. + expect(checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(true); + expect(checks.some((check) => check.code === "claude_hello_probe_auth_required")).toBe(true); + expect(checks.some((check) => check.code === "claude_acp_login_probe_unavailable")).toBe(false); + // The raw probe text, including the bearer marker, never reaches a check. + expect(JSON.stringify(checks)).not.toContain(invalidTokenMarker); + }); + + it("does not flag a healthy probe whose assistant text repeats a token phrase", async () => { + // A healthy run prints an auth phrase in its answer text. The parsed result + // is a success, so the probe stays healthy and offers no login gate. + probeResult.value = { + exitCode: 0, + stdout: [ + initLine, + '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hello — note authentication_failed means an invalid bearer token"}]},"session_id":"abc"}', + '{"type":"result","subtype":"success","is_error":false,"result":"hello","session_id":"abc"}', + ].join("\n"), + stderr: "", + timedOut: false, + }; + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + // The parsed result is a success, so no login gate and no probe-unavailable + // check appears. + expect(checks).toEqual([]); + }); + + it("keeps a non-auth failed probe with a token phrase on the probe-unavailable path", async () => { + // The probe fails on a transient upstream error and prints an auth phrase in + // its assistant text. The parsed result is not an auth failure, so the probe + // stays on the neutral probe-unavailable code, not the login gate. + probeResult.value = { + exitCode: 1, + stdout: [ + initLine, + '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"authentication_failed: the bearer token is invalid"}]},"session_id":"abc"}', + '{"type":"result","subtype":"error_during_execution","is_error":true,"result":"API Error: 503 service unavailable","session_id":"abc"}', + ].join("\n"), + stderr: "", + timedOut: false, + }; + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + expect(checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(false); + expect(checks.some((check) => check.code === "claude_acp_login_probe_unavailable")).toBe(true); + }); + + it("never renders an untrusted login URL with sensitive query or fragment text", async () => { + // The sandbox prints a login-required line that carries a malicious URL. The + // URL has a non-allowlisted host, a query, and a fragment, each with a + // marker. The normalizer must reject the URL, so no marker reaches a check. + probeResult.value = { + exitCode: 1, + stdout: [ + initLine, + '{"type":"result","subtype":"error_during_execution","is_error":true,"result":"Please run `claude login`. Visit https://evil.example.com/claude-login?leak=SECRETQUERYmarker#SECRETFRAGmarker","session_id":"abc"}', + ].join("\n"), + stderr: "", + timedOut: false, + }; + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + // The login-required checks still appear. + expect(checks.some((check) => check.code === "claude_hello_probe_auth_required")).toBe(true); + expect(checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(true); + const checkText = JSON.stringify(checks); + // No marker and no untrusted host reaches any check. + expect(checkText).not.toContain("SECRETQUERYmarker"); + expect(checkText).not.toContain("SECRETFRAGmarker"); + expect(checkText).not.toContain("evil.example.com"); + // The hint falls back to the fixed `claude login` text. + const authRequired = checks.find((check) => check.code === "claude_hello_probe_auth_required"); + expect(authRequired?.hint).toBe("Run `claude login` in this environment, then retry the probe."); + }); + + it("emits no checks when the sandbox probe reports a healthy login", async () => { + probeResult.value = { exitCode: 0, stdout: helloStdout, stderr: "", timedOut: false }; + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + expect(checks).toEqual([]); + }); + + it("emits a distinct warn check, not a silent pass, when the probe cannot run", async () => { + probeResult.throwError = new Error("claude command not found"); + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + // A probe that cannot run must not report a success. It emits a distinct + // warn check that is NOT the login affordance code. + expect(checks).toHaveLength(1); + expect(checks[0]?.code).toBe("claude_acp_login_probe_unavailable"); + expect(checks[0]?.level).toBe("warn"); + expect(checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(false); + }); + + it("emits a distinct warn check when the probe times out", async () => { + probeResult.value = { exitCode: null as unknown as number, stdout: "", stderr: "", timedOut: true }; + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + expect(checks).toHaveLength(1); + expect(checks[0]?.code).toBe("claude_acp_login_probe_unavailable"); + expect(checks[0]?.level).toBe("warn"); + expect(checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(false); + }); + + it("emits a distinct warn check when the probe runs but does not complete", async () => { + // The probe ran, login is not detected, and the exit code is non-zero. This + // is not a healthy login, so it must not be a silent pass. + probeResult.value = { + exitCode: 1, + stdout: initLine, + stderr: "unexpected sandbox error", + timedOut: false, + }; + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + expect(checks).toHaveLength(1); + expect(checks[0]?.code).toBe("claude_acp_login_probe_unavailable"); + expect(checks[0]?.level).toBe("warn"); + }); + + it("never copies a thrown probe error into a Test-result check", async () => { + // A sandbox transport failure can carry a credential. Inject a secret + // marker through the thrown error and assert no check text repeats it. + const secret = "sk-ant-LEAKMARKER0123456789abcdef"; + probeResult.throwError = new Error(`transport failed with ${secret}`); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + const checkText = JSON.stringify(checks); + expect(checkText).not.toContain(secret); + expect(checkText).not.toContain("LEAKMARKER"); + expect(checks[0]?.code).toBe("claude_acp_login_probe_unavailable"); + // The diagnostic still reaches the server log, but the secret is redacted. + expect(warnSpy).toHaveBeenCalledTimes(1); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).not.toContain(secret); + expect(loggedText).toContain("***REDACTED***"); + warnSpy.mockRestore(); + }); + + it("never copies raw probe stderr or stdout into a Test-result check", async () => { + // A non-zero probe can print a credential to stderr. Inject a secret marker + // and assert no check text repeats it. + const secret = "sk-ant-STDERRMARKER0123456789abcdef"; + probeResult.value = { + exitCode: 2, + stdout: initLine, + stderr: `fatal: leaked ${secret}`, + timedOut: false, + }; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const checks = await probeClaudeAcpSandboxLogin({ + config: { engine: "acp" }, + target: sandboxTarget, + }); + + const checkText = JSON.stringify(checks); + expect(checkText).not.toContain(secret); + expect(checkText).not.toContain("STDERRMARKER"); + expect(checks[0]?.code).toBe("claude_acp_login_probe_unavailable"); + // The diagnostic still reaches the server log, but the secret is redacted. + expect(warnSpy).toHaveBeenCalledTimes(1); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).not.toContain(secret); + expect(loggedText).toContain("***REDACTED***"); + warnSpy.mockRestore(); + }); +}); diff --git a/packages/adapters/claude-local/src/server/acp.ts b/packages/adapters/claude-local/src/server/acp.ts index f0dbc46b96..cb18a87eb2 100644 --- a/packages/adapters/claude-local/src/server/acp.ts +++ b/packages/adapters/claude-local/src/server/acp.ts @@ -17,7 +17,9 @@ import { ensureAdapterExecutionTargetCommandResolvable, readAdapterExecutionTarget, resolveAdapterExecutionTargetCwd, + runAdapterExecutionTargetProcess, } from "@paperclipai/adapter-utils/execution-target"; +import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target"; import { DEFAULT_ACP_ENGINE_MODE, DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS, @@ -30,6 +32,7 @@ import type { AcpxRemoteManagedHomeResult, } from "@paperclipai/adapter-utils/acpx-engine/execute"; import { + asBoolean, asNumber, asString, parseObject, @@ -37,7 +40,16 @@ import { import { materializeRemoteClaudeConfig, prepareClaudeConfigSeed, + prepareSandboxClaudeProbeRuntime, } from "./claude-config.js"; +import { + buildClaudeLoginRequiredHint, + logRedactedSandboxProbeDiagnostic, +} from "./probe-diagnostics.js"; +import { detectClaudeLoginRequired, parseClaudeStreamJson } from "./parse.js"; +import { buildClaudeProbePermissionArgs } from "./permissions.js"; +import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js"; +import { SANDBOX_INSTALL_COMMAND } from "../index.js"; const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const packageRootDir = path.resolve(moduleDir, "../.."); @@ -300,6 +312,36 @@ function withClaudeAcpDefaults(options: ClaudeAcpExecutorOptions): AcpxEngineExe }; } +/** + * The generic error code the shared acpx engine emits when a run fails because + * the agent has no ready authentication. The shared engine stays vendor-neutral, + * so it keeps this generic code. See `adapter-utils/acpx-engine/execute.ts`. + */ +const ACPX_AUTH_REQUIRED_ERROR_CODE = "acpx_auth_required"; + +/** + * The Claude-specific error code the user interface reads to show the Claude + * login affordance on a run. The Claude CLI lane already emits this code. See + * `execute.ts` and the user interface gate in `ui/src/pages/AgentDetail.tsx`. + */ +const CLAUDE_AUTH_REQUIRED_ERROR_CODE = "claude_auth_required"; + +/** + * Translate the generic acpx auth-required code into the Claude-specific code at + * the claude-local boundary. The shared acpx engine reports the generic + * `acpx_auth_required` code for every adapter. The user interface run gate reads + * the Claude-specific `claude_auth_required` code, the same code the Claude CLI + * lane emits. Without this translation the default ACP run never shows the login + * prompt. The function changes only the error code and keeps every other field, + * so the error message and the error metadata stay intact. + */ +export function mapClaudeAcpAuthErrorCode( + result: AdapterExecutionResult, +): AdapterExecutionResult { + if (result.errorCode !== ACPX_AUTH_REQUIRED_ERROR_CODE) return result; + return { ...result, errorCode: CLAUDE_AUTH_REQUIRED_ERROR_CODE }; +} + export function createClaudeAcpExecutor(options: ClaudeAcpExecutorOptions = {}): ClaudeAcpExecutor { let executor: ClaudeAcpExecutor | null = null; return async (ctx) => { @@ -309,10 +351,11 @@ export function createClaudeAcpExecutor(options: ClaudeAcpExecutorOptions = {}): currentExecutor = createAcpxEngineExecutor(withClaudeAcpDefaults(options)); executor = currentExecutor; } - return currentExecutor({ + const result = await currentExecutor({ ...ctx, config: buildClaudeAcpConfig(ctx.config), }); + return mapClaudeAcpAuthErrorCode(result); }; } @@ -450,6 +493,132 @@ function isNonEmpty(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } +/** + * Build the two checks that tell the user interface a sandbox has no ready + * Claude authentication. The first check is descriptive for diagnostics. The + * second check is the neutral canonical code the user interface reads to offer + * login. The user interface does not read the message text or the top-level + * status. + */ +function buildAcpSandboxAuthMissingChecks(loginUrl: string | null): AdapterEnvironmentCheck[] { + return [ + { + code: "claude_hello_probe_auth_required", + level: "warn", + message: "Claude ACP is available, but login is required.", + hint: buildClaudeLoginRequiredHint(loginUrl), + }, + { + code: ADAPTER_AUTH_MISSING_CHECK_CODE, + level: "warn", + message: "The sandbox has no ready authentication for this adapter.", + hint: "Provide credentials for this adapter, or start login in the sandbox.", + }, + ]; +} + +/** + * Build the check that tells the user a Claude login probe could not run on the + * ACP path. The check is a warn, not an info, so `summarizeStatus` never + * reports a pass. The check code is distinct from `adapter_auth_missing`, so the + * user interface never shows the login affordance for a probe that could not + * confirm the login state. Nicky's direction: a sandbox Test without available + * auth must not report a success. + */ +function buildAcpLoginProbeUnavailableCheck(message: string): AdapterEnvironmentCheck { + return { + code: "claude_acp_login_probe_unavailable", + level: "warn", + message, + hint: "Verify that the sandbox can run `claude` and retry the Test. Set engine=cli to use the Claude CLI lane.", + }; +} + +/** + * Probe the stored Claude login inside a sandbox on the ACP path. The ACP engine + * and the Claude CLI share the same stored Claude login, so the probe runs the + * `claude` command with a short hello turn. The caller passes the prepared + * `env`, so the probe reads the managed `CLAUDE_CONFIG_DIR` the same way the CLI + * lane does. When the probe reports that login is required, the function returns + * the canonical auth-missing checks. The user interface reads the canonical + * check to offer login on the default ACP path, the same way it does for the + * Claude CLI path. + * + * The function keeps two signals distinct. It returns `adapter_auth_missing` + * only when the probe ran and login is required. It returns a separate warn + * check when the probe could not run, timed out, or did not complete. It never + * maps "probe could not run" to a silent pass. + */ +export async function probeClaudeAcpSandboxLogin(input: { + config: Record; + target: AdapterExecutionTarget; + env?: Record; +}): Promise { + const { config, target } = input; + let env: Record; + if (input.env) { + env = input.env; + } else { + const envConfig = parseObject(config.env); + env = {}; + for (const [key, value] of Object.entries(envConfig)) { + if (typeof value === "string") env[key] = value; + } + } + const command = "claude"; + const args = ["--print", "-", "--output-format", "stream-json", "--verbose"]; + args.push( + ...buildClaudeProbePermissionArgs({ + dangerouslySkipPermissions: asBoolean(config.dangerouslySkipPermissions, true), + targetIsRemote: true, + localProcessUid: process.getuid?.() ?? null, + }), + ); + const timeoutSec = Math.max(1, asNumber(config.helloProbeTimeoutSec, 90)); + const runId = `claude-acp-authprobe-${Date.now()}-${Math.random().toString(16).slice(2)}`; + let probe: Awaited>; + try { + probe = await runAdapterExecutionTargetProcess(runId, target, command, args, { + cwd: target.kind === "remote" ? target.remoteCwd : process.cwd(), + env, + timeoutSec, + graceSec: 5, + stdin: "Respond with hello.", + onLog: async () => {}, + }); + } catch (err) { + // Keep the raw error out of the Test-result check. Send the redacted + // diagnostic to the server log instead. + logRedactedSandboxProbeDiagnostic( + "Claude ACP login probe could not run in the sandbox", + err instanceof Error ? err.message : String(err), + ); + return [buildAcpLoginProbeUnavailableCheck("The Claude login probe could not run in the sandbox.")]; + } + if (probe.timedOut) { + return [buildAcpLoginProbeUnavailableCheck("The Claude login probe timed out.")]; + } + const parsedStream = parseClaudeStreamJson(probe.stdout); + const loginMeta = detectClaudeLoginRequired({ + parsed: parsedStream.resultJson, + stdout: probe.stdout, + stderr: probe.stderr, + }); + if (loginMeta.requiresLogin) { + return buildAcpSandboxAuthMissingChecks(loginMeta.loginUrl); + } + if ((probe.exitCode ?? 1) !== 0) { + // Keep the raw sandbox stderr and stdout out of the Test-result check. Send + // the redacted diagnostic to the server log instead. + logRedactedSandboxProbeDiagnostic( + "Claude ACP login probe did not complete", + firstNonEmptyString(probe.stderr, probe.stdout), + ); + return [buildAcpLoginProbeUnavailableCheck("The Claude login probe did not complete.")]; + } + return []; +} + export async function testClaudeAcpEnvironment( ctx: AdapterEnvironmentTestContext, ): Promise { @@ -553,6 +722,45 @@ export async function testClaudeAcpEnvironment( }); } + // A sandbox target can start a login flow, and subscription auth is the only + // credential source left after the branches above rule out Bedrock and an + // API key. Prepare the sandbox the same way the CLI lane does — install the + // Claude CLI when it is absent and materialize the managed CLAUDE_CONFIG_DIR + // — then probe the stored Claude login. The Test result carries the canonical + // adapter_auth_missing signal when login is required, and a distinct warn + // check when the probe cannot run. The user interface reads the canonical + // signal to offer login on the default ACP path. + if ( + target?.kind === "remote" && + target.transport === "sandbox" && + !hasBedrock && + !isNonEmpty(configApiKey) + ) { + const probeEnv: Record = {}; + for (const [key, value] of Object.entries(envConfig)) { + if (typeof value === "string") probeEnv[key] = value; + } + const runId = `claude-acp-envtest-${Date.now()}-${Math.random().toString(16).slice(2)}`; + checks.push( + ...(await prepareSandboxClaudeProbeRuntime({ + runId, + target, + cwd, + companyId: ctx.companyId, + env: probeEnv, + installCommand: SANDBOX_INSTALL_COMMAND, + detectCommand: "claude", + targetIsRemote: true, + targetIsSandbox: true, + helloProbeTimeoutSec: asNumber(config.helloProbeTimeoutSec, 90), + })), + ); + const canProbe = !checks.some((check) => check.code === "claude_managed_config_dir_failed"); + if (canProbe) { + checks.push(...(await probeClaudeAcpSandboxLogin({ config, target, env: probeEnv }))); + } + } + const mode = firstNonEmptyString(config.mode, config.acpMode) ?? DEFAULT_ACP_ENGINE_MODE; const warmHandleIdleMs = asNumber( config.warmHandleIdleMs ?? config.acpWarmHandleIdleMs, diff --git a/packages/adapters/claude-local/src/server/auth-check.ts b/packages/adapters/claude-local/src/server/auth-check.ts new file mode 100644 index 0000000000..9e606eb149 --- /dev/null +++ b/packages/adapters/claude-local/src/server/auth-check.ts @@ -0,0 +1,12 @@ +/** + * The canonical check code the Claude Test engine emits when a sandbox target + * has no ready authentication. The user interface reads this stable code to + * decide login eligibility. The user interface does not read the message text + * or the top-level status. + * + * The name is neutral. It carries no vendor name and no login-flow word, because + * a Test result can sync to public packages. The value matches the code the + * Codex Test engine emits, so the user interface can gate login from one code + * across adapters. + */ +export const ADAPTER_AUTH_MISSING_CHECK_CODE = "adapter_auth_missing"; diff --git a/packages/adapters/claude-local/src/server/claude-config.ts b/packages/adapters/claude-local/src/server/claude-config.ts index f600c8b219..b5ed22886f 100644 --- a/packages/adapters/claude-local/src/server/claude-config.ts +++ b/packages/adapters/claude-local/src/server/claude-config.ts @@ -2,14 +2,22 @@ import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AdapterExecutionContext, AdapterRuntimeMcpServer } from "@paperclipai/adapter-utils"; +import type { + AdapterExecutionContext, + AdapterEnvironmentCheck, + AdapterRuntimeMcpServer, +} from "@paperclipai/adapter-utils"; import { + adapterExecutionTargetUsesManagedHome, + maybeRunSandboxInstallCommand, + prepareAdapterExecutionTargetRuntime, runAdapterExecutionTargetShellCommand, type AdapterExecutionTarget, type AdapterExecutionTargetShellOptions, } from "@paperclipai/adapter-utils/execution-target"; import { resolvePaperclipInstanceRootForAdapter } from "@paperclipai/adapter-utils/server-utils"; import { shellQuote } from "@paperclipai/adapter-utils/ssh"; +import { logRedactedSandboxProbeDiagnostic } from "./probe-diagnostics.js"; const SEEDED_SHARED_FILES = ["settings.json", "CLAUDE.md"] as const; @@ -242,3 +250,124 @@ export async function materializeRemoteClaudeConfig(input: { input.options, ); } + +function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +/** + * Prepare the sandbox runtime that a Claude hello probe needs. The step + * installs the Claude CLI in the sandbox when the CLI is absent, and it + * materializes the Paperclip-managed Claude config directory. Both the CLI + * Test lane and the ACP Test lane call this helper, so the two lanes probe + * the same login state. The Claude CLI and the Claude ACP engine share the + * same stored Claude login. + * + * The function mutates `env`: it sets `CLAUDE_CONFIG_DIR` to the managed + * remote config directory when it materializes one. It returns the checks to + * add to the Test result. An operator-provided `CLAUDE_CONFIG_DIR` wins, so + * the function keeps it and skips the managed materialization. + */ +export async function prepareSandboxClaudeProbeRuntime(input: { + runId: string; + target: AdapterExecutionTarget | null; + cwd: string; + companyId?: string; + env: Record; + installCommand: string; + detectCommand: string; + targetIsRemote: boolean; + targetIsSandbox: boolean; + helloProbeTimeoutSec: number; +}): Promise { + const checks: AdapterEnvironmentCheck[] = []; + const installCheck = await maybeRunSandboxInstallCommand({ + runId: input.runId, + target: input.target, + adapterKey: "claude", + installCommand: input.installCommand, + detectCommand: input.detectCommand, + env: input.env, + }); + if (installCheck) checks.push(installCheck); + + const hasExplicitClaudeConfigDir = isNonEmptyString(input.env.CLAUDE_CONFIG_DIR); + if ( + input.targetIsRemote && + adapterExecutionTargetUsesManagedHome(input.target) && + !hasExplicitClaudeConfigDir + ) { + let tempWorkspaceDir: string | null = null; + let preparedRuntime: Awaited> | null = null; + try { + const seedDir = await prepareClaudeConfigSeed(process.env, async () => {}, input.companyId); + const managedRemoteCwd = + input.target?.kind === "remote" ? input.target.remoteCwd : input.cwd; + tempWorkspaceDir = await fs.mkdtemp( + path.join(os.tmpdir(), "paperclip-claude-envtest-workspace-"), + ); + preparedRuntime = await prepareAdapterExecutionTargetRuntime({ + runId: input.runId, + target: input.target, + adapterKey: "claude", + workspaceLocalDir: tempWorkspaceDir, + workspaceRemoteDir: managedRemoteCwd, + timeoutSec: Math.max(1, input.helloProbeTimeoutSec), + assets: [ + { + key: "config-seed", + localDir: seedDir, + followSymlinks: true, + }, + ], + }); + const runtimeRootDir = + preparedRuntime.runtimeRootDir ?? + path.posix.join(managedRemoteCwd, ".paperclip-runtime", "claude"); + const remoteClaudeConfigSeedDir = + preparedRuntime.assetDirs["config-seed"] ?? + path.posix.join(runtimeRootDir, "config-seed"); + const remoteClaudeConfigDir = path.posix.join(runtimeRootDir, "config"); + input.env.CLAUDE_CONFIG_DIR = remoteClaudeConfigDir; + await materializeRemoteClaudeConfig({ + runId: input.runId, + target: input.target, + remoteClaudeConfigDir, + remoteClaudeConfigSeedDir, + options: { + cwd: input.cwd, + env: input.env, + timeoutSec: Math.max(15, input.helloProbeTimeoutSec), + graceSec: 5, + onLog: async () => {}, + }, + }); + checks.push({ + code: "claude_managed_config_dir", + level: "info", + message: "Sandbox probe is using Paperclip-managed Claude config materialization.", + detail: remoteClaudeConfigDir, + }); + } catch (err) { + // Keep the raw error out of the Test-result check. Send the redacted + // diagnostic to the server log instead. + logRedactedSandboxProbeDiagnostic( + "Could not materialize Paperclip-managed Claude config for the sandbox probe", + err instanceof Error ? err.message : String(err), + ); + checks.push({ + code: "claude_managed_config_dir_failed", + level: "error", + message: "Could not materialize Paperclip-managed Claude config for the sandbox probe.", + hint: "Retry the Test. If the failure repeats, check the server log for the redacted diagnostic.", + }); + } finally { + await preparedRuntime?.restoreWorkspace().catch(() => undefined); + if (tempWorkspaceDir) { + await fs.rm(tempWorkspaceDir, { recursive: true, force: true }).catch(() => undefined); + } + } + } + + return checks; +} diff --git a/packages/adapters/claude-local/src/server/index.ts b/packages/adapters/claude-local/src/server/index.ts index 801923f395..bb677eb6f1 100644 --- a/packages/adapters/claude-local/src/server/index.ts +++ b/packages/adapters/claude-local/src/server/index.ts @@ -36,8 +36,8 @@ export { parseSetupTokenPrompt, parseSetupTokenCredential, SETUP_TOKEN_PROMPT, - SETUP_TOKEN_URL_ORIGIN, - SETUP_TOKEN_URL_PATH, + SETUP_TOKEN_AUTH_URLS, + SETUP_TOKEN_REDIRECT_URI, SETUP_TOKEN_URL_QUERY_KEYS, SETUP_TOKEN_PREFIX, SETUP_TOKEN_BEFORE_ANCHOR, @@ -52,7 +52,6 @@ export { CLAUDE_SETUP_TOKEN_COMMAND, CODE_SUBMISSION_TERMINATOR, CLAUDE_SETUP_TOKEN_MAX_BUFFER_CHARS, - SETUP_TOKEN_CREDENTIAL_RELEASE_GATE, } from "./setup-token-runner.js"; export type { SetupTokenPtyDriver, diff --git a/packages/adapters/claude-local/src/server/parse.test.ts b/packages/adapters/claude-local/src/server/parse.test.ts index 9e88e5a8c9..13b4964b99 100644 --- a/packages/adapters/claude-local/src/server/parse.test.ts +++ b/packages/adapters/claude-local/src/server/parse.test.ts @@ -33,6 +33,118 @@ describe("detectClaudeLoginRequired", () => { }).requiresLogin, ).toBe(false); }); + + it("classifies an invalid or expired OAuth bearer token as login required", () => { + // Grounded on the real Claude CLI output for CLAUDE_CODE_OAUTH_TOKEN=invalid: + // the result event carries a 401 authentication failure and an "Invalid + // bearer token" message. This is an auth failure, not a probe that could not + // run, so the detector must classify it as login required. + const parsed = { + is_error: true, + subtype: "success", + api_error_status: 401, + error: "authentication_failed", + result: "Failed to authenticate. API Error: 401 Invalid bearer token", + }; + expect( + detectClaudeLoginRequired({ + parsed, + stdout: + '{"type":"assistant","message":{"content":[{"type":"text","text":"Failed to authenticate. API Error: 401 Invalid bearer token"}]},"error":"authentication_failed"}', + stderr: "", + }).requiresLogin, + ).toBe(true); + }); + + it("does not route an invalid token to the transient or quota classifiers", () => { + // An auth failure must win over the transient and quota lanes, so a run + // surfaces login required instead of a retry. + const input = { + parsed: { + is_error: true, + result: "Failed to authenticate. API Error: 401 Invalid bearer token", + }, + stdout: "", + stderr: "", + }; + expect(isClaudeTransientUpstreamError(input)).toBe(false); + expect(isClaudeProviderQuotaError(input)).toBe(false); + }); + + it("classifies an expired or revoked token in the parsed result as login required", () => { + // The result event marks the run as a failure and reports an expired token. + // This is a token failure, so the detector classifies it as login required. + const parsed = { + is_error: true, + subtype: "success", + result: "Failed to authenticate. Your OAuth access token is expired.", + }; + expect( + detectClaudeLoginRequired({ parsed, stdout: "", stderr: "" }).requiresLogin, + ).toBe(true); + }); + + it("does not classify a successful probe whose assistant text repeats a token phrase", () => { + // A healthy run can print an auth phrase in its answer text. The parsed + // result is a success, so the token-failure markers must not fire on the raw + // stdout assistant event. + const parsed = { + is_error: false, + subtype: "success", + result: "hello", + }; + expect( + detectClaudeLoginRequired({ + parsed, + stdout: + '{"type":"assistant","message":{"content":[{"type":"text","text":"The phrase authentication_failed means an invalid bearer token."}]}}', + stderr: "", + }).requiresLogin, + ).toBe(false); + }); + + it("does not classify a success whose result text repeats a token phrase", () => { + // The model's own answer repeats a token phrase, so the phrase lands in the + // parsed result. The run is a success, so the failure gate keeps it healthy. + const parsed = { + is_error: false, + subtype: "success", + result: "Sure. An invalid bearer token means authentication_failed.", + }; + expect( + detectClaudeLoginRequired({ parsed, stdout: "", stderr: "" }).requiresLogin, + ).toBe(false); + }); + + it("keeps a transient failure with an assistant token phrase on the transient lane", () => { + // The run fails on a 503 transient error. The token phrase appears only in + // the raw stdout assistant event, not the parsed result, so the run stays + // transient and never classifies as login required. + const input = { + parsed: { + is_error: true, + subtype: "error_during_execution", + result: "API Error: 503 service unavailable", + }, + stdout: + '{"type":"assistant","message":{"content":[{"type":"text","text":"authentication_failed: the bearer token is invalid"}]}}', + stderr: "", + }; + expect(detectClaudeLoginRequired(input).requiresLogin).toBe(false); + expect(isClaudeTransientUpstreamError(input)).toBe(true); + }); + + it("does not treat a bare token phrase in raw stdout with no parsed result as login required", () => { + // Untrusted stdout alone must not satisfy a token-failure marker. Only the + // parsed terminal result fields of a failed run can trip the token markers. + expect( + detectClaudeLoginRequired({ + parsed: null, + stdout: "authentication_failed while the assistant called a tool", + stderr: "", + }).requiresLogin, + ).toBe(false); + }); }); describe("isClaudeModelNotFoundError", () => { diff --git a/packages/adapters/claude-local/src/server/parse.ts b/packages/adapters/claude-local/src/server/parse.ts index 16e875acff..a3f3d95bec 100644 --- a/packages/adapters/claude-local/src/server/parse.ts +++ b/packages/adapters/claude-local/src/server/parse.ts @@ -2,11 +2,22 @@ import type { UsageSummary } from "@paperclipai/adapter-utils"; import { asString, asNumber, + asBoolean, parseObject, parseJson, } from "@paperclipai/adapter-utils/server-utils"; -const CLAUDE_AUTH_REQUIRED_RE = /(?:not\s+logged\s+in|please\s+log\s+in|please\s+run\s+(?:`?claude\s+login`?|\/login)|login\s+required|requires\s+login|unauthorized|authentication\s+required|invalid\s+api\s+key[\s\S]{0,120}(?:\/login|claude\s+login|log\s+in))/i; +// The legacy login-prompt markers. The Claude CLI prints these words when it +// asks the user to log in. The detector matches them against any probe output +// line, which includes the raw stdout and stderr. This scope is pre-existing. +const CLAUDE_LOGIN_PROMPT_RE = + /(?:not\s+logged\s+in|please\s+log\s+in|please\s+run\s+(?:`?claude\s+login`?|\/login)|login\s+required|requires\s+login|unauthorized|authentication\s+required|invalid\s+api\s+key[\s\S]{0,120}(?:\/login|claude\s+login|log\s+in))/i; + +// The token-failure markers. An assistant or model event can print these same +// words as ordinary prose, so the detector matches them only against the parsed +// terminal result fields of a failed run. See detectClaudeLoginRequired. +const CLAUDE_AUTH_TOKEN_FAILURE_RE = + /(?:authentication[_\s-](?:failed|error)|failed\s+to\s+authenticate|invalid\s+bearer\s+token|(?:invalid|expired|revoked)[\s\S]{0,40}(?:bearer|oauth|access)\s+token|(?:bearer|oauth|access)\s+token[\s\S]{0,40}(?:is\s+)?(?:invalid|expired|revoked))/i; const URL_RE = /(https?:\/\/[^\s'"`<>()[\]{};,!?]+[^\s'"`<>()[\]{};,!.?:]+)/gi; const CLAUDE_TRANSIENT_UPSTREAM_RE = @@ -163,21 +174,63 @@ export function extractClaudeLoginUrl(text: string): string | null { return match[0]?.replace(/[\])}.!,?;:'\"]+$/g, "") ?? null; } +// Collect the parsed terminal result fields that carry an auth failure. The +// CLI writes the token-failure text to the result event, so the detector reads +// the result string, the top-level error field, and the errors array. It never +// reads the raw stdout, so an assistant event cannot inject a token marker. +function collectClaudeTerminalText(parsed: Record): string { + return [ + asString(parsed.result, ""), + asString(parsed.error, ""), + ...extractClaudeErrorMessages(parsed), + ] + .map((field) => field.trim()) + .filter(Boolean) + .join("\n"); +} + +// Report whether the parsed terminal result marks the run as an auth failure. +// The token-failure markers apply only to a failed run. A successful probe +// whose answer text repeats an auth phrase does not classify as login required. +function claudeResultIndicatesAuthFailure(parsed: Record): boolean { + if (asBoolean(parsed.is_error, false)) return true; + const subtype = asString(parsed.subtype, "").trim().toLowerCase(); + if (subtype.startsWith("error")) return true; + const status = + asNumber(parsed.api_error_status, 0) || asNumber(parsed.error_status, 0); + if (status === 401 || status === 403) return true; + if (asString(parsed.error, "").trim()) return true; + return extractClaudeErrorMessages(parsed).length > 0; +} + export function detectClaudeLoginRequired(input: { parsed: Record | null; stdout: string; stderr: string; }): { requiresLogin: boolean; loginUrl: string | null } { - const resultText = asString(input.parsed?.result, "").trim(); - const messages = [resultText, ...extractClaudeErrorMessages(input.parsed ?? {}), input.stdout, input.stderr] + const parsed = input.parsed ?? null; + const resultText = asString(parsed?.result, "").trim(); + + // The legacy login-prompt markers keep their broad scope. They match against + // every output line, which includes the parsed result, the parsed errors, and + // the raw stdout and stderr. + const promptLines = [resultText, ...extractClaudeErrorMessages(parsed ?? {}), input.stdout, input.stderr] .join("\n") .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); + const loginPrompt = promptLines.some((line) => CLAUDE_LOGIN_PROMPT_RE.test(line)); + + // The token-failure markers match only against the parsed terminal fields of + // a failed run. The raw stdout is untrusted, so a model that prints a token + // phrase, or a successful run that repeats one, does not flip the classifier. + const tokenFailure = + parsed !== null && + claudeResultIndicatesAuthFailure(parsed) && + CLAUDE_AUTH_TOKEN_FAILURE_RE.test(collectClaudeTerminalText(parsed)); - const requiresLogin = messages.some((line) => CLAUDE_AUTH_REQUIRED_RE.test(line)); return { - requiresLogin, + requiresLogin: loginPrompt || tokenFailure, loginUrl: extractClaudeLoginUrl([input.stdout, input.stderr].join("\n")), }; } diff --git a/packages/adapters/claude-local/src/server/probe-diagnostics.test.ts b/packages/adapters/claude-local/src/server/probe-diagnostics.test.ts new file mode 100644 index 0000000000..0998a4f073 --- /dev/null +++ b/packages/adapters/claude-local/src/server/probe-diagnostics.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it, vi } from "vitest"; +import { + buildClaudeLoginRequiredHint, + logRedactedSandboxProbeDiagnostic, + normalizeClaudeLoginUrl, +} from "./probe-diagnostics.js"; + +describe("normalizeClaudeLoginUrl", () => { + it("accepts an allowlisted https Claude host with no query or fragment", () => { + expect(normalizeClaudeLoginUrl("https://claude.ai/login")).toBe("https://claude.ai/login"); + }); + + it("accepts an allowlisted Anthropic subdomain", () => { + expect(normalizeClaudeLoginUrl("https://console.anthropic.com/login")).toBe( + "https://console.anthropic.com/login", + ); + }); + + it("rejects a URL with a query", () => { + expect(normalizeClaudeLoginUrl("https://claude.ai/login?token=secret")).toBeNull(); + }); + + it("rejects a URL with a fragment", () => { + expect(normalizeClaudeLoginUrl("https://claude.ai/login#access_token=secret")).toBeNull(); + }); + + it("rejects a non-https URL", () => { + expect(normalizeClaudeLoginUrl("http://claude.ai/login")).toBeNull(); + }); + + it("rejects a non-allowlisted host", () => { + expect(normalizeClaudeLoginUrl("https://evil.example.com/claude-login")).toBeNull(); + }); + + it("rejects a look-alike host that only ends with the brand word", () => { + expect(normalizeClaudeLoginUrl("https://claude.ai.evil.com/login")).toBeNull(); + expect(normalizeClaudeLoginUrl("https://notclaude.ai/login")).toBeNull(); + }); + + it("rejects a URL that embeds credentials or a port", () => { + expect(normalizeClaudeLoginUrl("https://user:pass@claude.ai/login")).toBeNull(); + expect(normalizeClaudeLoginUrl("https://claude.ai:8443/login")).toBeNull(); + }); + + it("returns null for empty or malformed input", () => { + expect(normalizeClaudeLoginUrl(null)).toBeNull(); + expect(normalizeClaudeLoginUrl(undefined)).toBeNull(); + expect(normalizeClaudeLoginUrl("not a url")).toBeNull(); + }); +}); + +describe("buildClaudeLoginRequiredHint", () => { + it("names a safe login URL in the hint", () => { + expect(buildClaudeLoginRequiredHint("https://claude.ai/login")).toBe( + "Run `claude login` and complete sign-in at https://claude.ai/login, then retry.", + ); + }); + + it("falls back to the fixed hint for an unsafe URL", () => { + expect(buildClaudeLoginRequiredHint("https://evil.example.com/claude-login?leak=secret")).toBe( + "Run `claude login` in this environment, then retry the probe.", + ); + }); + + it("falls back to the fixed hint for a null URL", () => { + expect(buildClaudeLoginRequiredHint(null)).toBe( + "Run `claude login` in this environment, then retry the probe.", + ); + }); +}); + +describe("logRedactedSandboxProbeDiagnostic", () => { + it("redacts a JSON secret field before it reaches the log", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + logRedactedSandboxProbeDiagnostic("probe failed", '{"token":"opaque-secret-value"}'); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).not.toContain("opaque-secret-value"); + expect(loggedText).toContain("***REDACTED***"); + warnSpy.mockRestore(); + }); + + it("does not log when the diagnostic is empty", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + logRedactedSandboxProbeDiagnostic("probe failed", ""); + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); + + it("redacts a JSON secret value with an escaped quote before it reaches the log", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + // The value holds an escaped quote, then the rest of the credential. A naive + // matcher stops at the escaped quote and leaks the marker to the log. + logRedactedSandboxProbeDiagnostic("probe failed", '{"token":"pre\\"MARKERLOGQUOTE"}'); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).not.toContain("MARKERLOGQUOTE"); + expect(loggedText).toContain("***REDACTED***"); + warnSpy.mockRestore(); + }); + + it("redacts an escaped-JSON secret value before it reaches the log", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const innerJson = '{"password":"pre\\\\MARKERLOGBACKSLASH"}'; + logRedactedSandboxProbeDiagnostic("probe failed", JSON.stringify(innerJson)); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).not.toContain("MARKERLOGBACKSLASH"); + expect(loggedText).toContain("***REDACTED***"); + warnSpy.mockRestore(); + }); +}); diff --git a/packages/adapters/claude-local/src/server/probe-diagnostics.ts b/packages/adapters/claude-local/src/server/probe-diagnostics.ts new file mode 100644 index 0000000000..0988e9cdd1 --- /dev/null +++ b/packages/adapters/claude-local/src/server/probe-diagnostics.ts @@ -0,0 +1,87 @@ +import { redactDiagnosticText } from "@paperclipai/adapter-utils"; + +// The server log keeps a bounded diagnostic. The bound stops a very large probe +// output from filling the log. +const MAX_LOGGED_PROBE_DIAGNOSTIC_CHARS = 2000; + +// The login hint may show a login URL. The URL must be a normalized https URL +// with an allowlisted Claude or Anthropic host and no query or fragment. A host +// matches when it equals a suffix or ends with a dot and the suffix. +const ALLOWED_LOGIN_URL_HOST_SUFFIXES = ["anthropic.com", "claude.ai"] as const; + +/** + * Send a sandbox probe or config materialization diagnostic to the server log. + * + * Three Test-lane call sites use this helper: + * - the Claude CLI Test lane (`test.ts`), + * - the Claude ACP Test lane (`acp.ts`), + * - the managed-config materialization step (`claude-config.ts`). + * + * The helper is the single boundary where a raw probe error, stdout, or stderr + * string reaches an output. It redacts secrets with `redactDiagnosticText` + * first, so no credential reaches the log. The sanitizer redacts shell + * `KEY=value` secrets and JSON secret fields such as `{"token":"..."}`. The + * helper also bounds the length. A caller must never copy the raw string into a + * Test-result check, because the user interface renders check text. + * + * @param context A short fixed description of the failed step. It carries no + * untrusted text. + * @param raw The untrusted diagnostic from the sandbox. The helper redacts it. + */ +export function logRedactedSandboxProbeDiagnostic( + context: string, + raw: string | null | undefined, +): void { + if (!raw) return; + const redacted = redactDiagnosticText(raw) + .replace(/\s+/g, " ") + .trim() + .slice(0, MAX_LOGGED_PROBE_DIAGNOSTIC_CHARS); + if (!redacted) return; + console.warn(`[paperclip] ${context}`, { detail: redacted }); +} + +/** + * Normalize an untrusted login URL for a Test-result hint. + * + * The extractor reads the URL from raw sandbox stdout or stderr, so the value is + * untrusted. The function accepts the URL only when every rule holds: + * - the protocol is `https`, + * - the host equals or ends with an allowlisted Claude or Anthropic host, + * - the URL has no user, password, or port, + * - the URL has no query and no fragment. + * + * The function returns the normalized URL string, or `null` when any rule + * fails. The caller shows a fixed `claude login` hint when the function returns + * `null`. + */ +export function normalizeClaudeLoginUrl(raw: string | null | undefined): string | null { + if (!raw) return null; + let url: URL; + try { + url = new URL(raw); + } catch { + return null; + } + if (url.protocol !== "https:") return null; + if (url.username || url.password || url.port) return null; + if (url.search || url.hash) return null; + const host = url.hostname.toLowerCase(); + const allowed = ALLOWED_LOGIN_URL_HOST_SUFFIXES.some( + (suffix) => host === suffix || host.endsWith(`.${suffix}`), + ); + if (!allowed) return null; + return url.toString(); +} + +/** + * Build the fixed auth-required hint for a Test-result check. When the login URL + * normalizes to a safe value, the hint names it. Otherwise the hint tells the + * operator to run `claude login`. + */ +export function buildClaudeLoginRequiredHint(loginUrl: string | null | undefined): string { + const safeUrl = normalizeClaudeLoginUrl(loginUrl); + return safeUrl + ? `Run \`claude login\` and complete sign-in at ${safeUrl}, then retry.` + : "Run `claude login` in this environment, then retry the probe."; +} diff --git a/packages/adapters/claude-local/src/server/probe-redaction.test.ts b/packages/adapters/claude-local/src/server/probe-redaction.test.ts new file mode 100644 index 0000000000..623f4cd0a1 --- /dev/null +++ b/packages/adapters/claude-local/src/server/probe-redaction.test.ts @@ -0,0 +1,97 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target"; + +// The managed-config step runs inside `prepareSandboxClaudeProbeRuntime`. The +// step resolves the Paperclip instance root first. This mock makes that resolve +// throw, so the managed-config materialization fails with a controllable error +// that carries a secret marker. +const { resolveInstanceRoot } = vi.hoisted(() => { + const resolveInstanceRoot: { throwError: Error | null } = { throwError: null }; + return { resolveInstanceRoot }; +}); + +vi.mock("@paperclipai/adapter-utils/execution-target", async () => { + const actual = await vi.importActual( + "@paperclipai/adapter-utils/execution-target", + ); + return { + ...actual, + maybeRunSandboxInstallCommand: vi.fn(async () => null), + }; +}); + +vi.mock("@paperclipai/adapter-utils/server-utils", async () => { + const actual = await vi.importActual( + "@paperclipai/adapter-utils/server-utils", + ); + return { + ...actual, + resolvePaperclipInstanceRootForAdapter: (...args: unknown[]) => { + if (resolveInstanceRoot.throwError) throw resolveInstanceRoot.throwError; + return ( + actual.resolvePaperclipInstanceRootForAdapter as (...a: unknown[]) => string + )(...args); + }, + }; +}); + +import { prepareSandboxClaudeProbeRuntime } from "./claude-config.js"; + +const sandboxTarget: AdapterExecutionTarget = { + kind: "remote", + transport: "sandbox", + providerKey: "daytona", + remoteCwd: "/home/daytona/paperclip-workspace", + runner: { + execute: async () => ({ + exitCode: 0, + signal: null, + timedOut: false, + stdout: "", + stderr: "", + pid: null, + startedAt: new Date().toISOString(), + }), + }, +}; + +afterEach(() => { + vi.clearAllMocks(); + resolveInstanceRoot.throwError = null; +}); + +describe("prepareSandboxClaudeProbeRuntime managed-config redaction", () => { + it("never copies a materialization error into a Test-result check", async () => { + // A materialization failure can carry a credential. Inject a secret marker + // through the thrown error and assert no check text repeats it. + const secret = "sk-ant-MANAGEDMARKER0123456789abcdef"; + resolveInstanceRoot.throwError = new Error(`materialize failed with ${secret}`); + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const checks = await prepareSandboxClaudeProbeRuntime({ + runId: "test-run", + target: sandboxTarget, + cwd: "/home/daytona/paperclip-workspace", + env: {}, + installCommand: "npm i -g @anthropic-ai/claude-code", + detectCommand: "claude --version", + targetIsRemote: true, + targetIsSandbox: true, + helloProbeTimeoutSec: 5, + }); + + const failedCheck = checks.find((check) => check.code === "claude_managed_config_dir_failed"); + expect(failedCheck).toBeDefined(); + expect(failedCheck?.level).toBe("error"); + + const checkText = JSON.stringify(checks); + expect(checkText).not.toContain(secret); + expect(checkText).not.toContain("MANAGEDMARKER"); + // The diagnostic still reaches the server log, but the secret is redacted. + expect(warnSpy).toHaveBeenCalledTimes(1); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).not.toContain(secret); + expect(loggedText).toContain("***REDACTED***"); + warnSpy.mockRestore(); + }); +}); diff --git a/packages/adapters/claude-local/src/server/setup-token-characterization.test.ts b/packages/adapters/claude-local/src/server/setup-token-characterization.test.ts new file mode 100644 index 0000000000..ed1c15363f --- /dev/null +++ b/packages/adapters/claude-local/src/server/setup-token-characterization.test.ts @@ -0,0 +1,145 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; +import { + parseSetupTokenPrompt, + SETUP_TOKEN_AUTH_URLS, + SETUP_TOKEN_PROMPT, + SETUP_TOKEN_URL_QUERY_KEYS, +} from "./setup-token-parse.js"; + +// The live characterization of `claude setup-token`. This suite runs the real +// Claude Code login command in a real pseudo-terminal, captures the sign-in +// prompt output, and confirms the parser reads the real terminal contract. It +// records the tested Claude Code version. It fails loudly on a parse miss, so a +// terminal-contract drift breaks the suite instead of passing silently. +// +// The suite is opt-in. It needs the real `claude` binary, `python3` for the +// pseudo-terminal, and network access to claude.com to mint the authorization +// URL, so it never runs in a normal or a continuous-integration test run. Set +// `RUN_CLAUDE_SETUP_TOKEN_CHARACTERIZATION=1` to run it in a real sandbox. +// +// Security: the suite drives only the prompt phase. It never submits a browser +// code, so the command never mints a token. It parses the captured bytes in +// memory and asserts only the non-secret shape (the URL origin, the URL path, +// the URL query keys, and the constant prompt). It never writes the captured +// bytes, the real authorization URL, or any token to a durable file, a log, or +// an assertion message. + +const OPT_IN = process.env.RUN_CLAUDE_SETUP_TOKEN_CHARACTERIZATION === "1"; + +// A pseudo-terminal harness. It runs `claude setup-token` on a real +// pseudo-terminal with a wide window, streams the raw terminal bytes to its own +// standard output, and stops when it sees the prompt phrase or after a timeout. +// It never submits a code, so the command never mints a token. +const PTY_HARNESS = ` +import os, pty, sys, select, time, fcntl, termios, struct, signal +duration = float(os.environ.get("STCHAR_DURATION", "40")) +cols, rows = 200, 50 +pid, fd = pty.fork() +if pid == 0: + os.execvp("claude", ["claude", "setup-token"]) + os._exit(127) +fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0)) +acc = b"" +start = time.time() +try: + while time.time() - start < duration: + r, _, _ = select.select([fd], [], [], 0.5) + if fd in r: + try: + data = os.read(fd, 65536) + except OSError: + break + if not data: + break + acc += data + os.write(1, data) + if b"Paste code here if prompted" in acc: + break +finally: + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + try: + os.waitpid(pid, 0) + except ChildProcessError: + pass +`; + +function hasCommand(command: string): boolean { + const probe = spawnSync("sh", ["-c", `command -v ${command}`], { encoding: "utf8" }); + return probe.status === 0; +} + +function readClaudeVersion(): string { + const raw = execFileSync("claude", ["--version"], { encoding: "utf8" }); + // The version line reads like `2.1.226 (Claude Code)`. Read the leading + // semantic version. + const match = raw.match(/(\d+\.\d+\.\d+)/); + if (!match) throw new Error("could not read the Claude Code version"); + return match[1]; +} + +// Removes the terminal control sequences the same way the parser does, so the +// test can assert on the rendered banner text. It keeps this local, so the test +// never imports a parser internal. +function renderForBanner(raw: string): string { + return raw + // OSC sequences (hyperlinks and other OSC), BEL or ST terminated. + .replace(/\x1b\][^\x1b\x07]*(?:\x1b\\|\x07)/g, "") + // Horizontal-move CSI sequences render as one space. + .replace(/\x1b\[[0-9;]*[GC]/g, " ") + // Any remaining CSI sequence. + .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, ""); +} + +const run = OPT_IN ? describe : describe.skip; + +run("claude setup-token live characterization", () => { + it( + "reads the real sign-in prompt contract and records the Claude Code version", + () => { + expect(hasCommand("claude"), "the claude binary must be installed").toBe(true); + expect(hasCommand("python3"), "python3 must be installed for the pseudo-terminal").toBe(true); + + const version = readClaudeVersion(); + + const result = spawnSync("python3", ["-c", PTY_HARNESS], { + encoding: "buffer", + maxBuffer: 8 * 1024 * 1024, + env: { ...process.env, STCHAR_DURATION: "40" }, + }); + if (result.error) throw result.error; + const raw = (result.stdout ?? Buffer.alloc(0)).toString("utf8"); + + // Fail loudly on a parse miss. A terminal-contract drift breaks the suite + // here instead of passing silently. + const parsed = parseSetupTokenPrompt(raw); + expect(parsed, "the parser did not read the live setup-token prompt").not.toBeNull(); + + const url = new URL(parsed!.url); + // The live CLI emits one of the two accepted origin-and-path pairs. The + // installed version decides which pair the run records. + expect(SETUP_TOKEN_AUTH_URLS).toContain(`${url.origin}${url.pathname}`); + expect([...url.searchParams.keys()].sort()).toEqual([...SETUP_TOKEN_URL_QUERY_KEYS].sort()); + expect(url.hash).toBe(""); + expect(parsed!.prompt).toBe(SETUP_TOKEN_PROMPT); + + // The banner names the tested Claude Code version. This ties the recorded + // contract to the version that produced it. + const banner = renderForBanner(raw); + expect(banner).toContain(`Claude Code v${version}`); + + // Record the tested version and the non-secret anchors. The reporter shows + // this line; it names no secret. + // eslint-disable-next-line no-console + console.log( + `[characterization] Claude Code v${version}: prompt "${SETUP_TOKEN_PROMPT}", ` + + `URL ${url.origin}${url.pathname} with query keys ` + + `[${[...SETUP_TOKEN_URL_QUERY_KEYS].join(", ")}].`, + ); + }, + 60_000, + ); +}); diff --git a/packages/adapters/claude-local/src/server/setup-token-parse.test.ts b/packages/adapters/claude-local/src/server/setup-token-parse.test.ts index 85171dc4b2..b703188062 100644 --- a/packages/adapters/claude-local/src/server/setup-token-parse.test.ts +++ b/packages/adapters/claude-local/src/server/setup-token-parse.test.ts @@ -4,10 +4,11 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { SETUP_TOKEN_AFTER_ANCHOR, + SETUP_TOKEN_AUTH_URLS, SETUP_TOKEN_BEFORE_ANCHOR, SETUP_TOKEN_PREFIX, SETUP_TOKEN_PROMPT, - SETUP_TOKEN_URL_PATH, + SETUP_TOKEN_REDIRECT_URI, SETUP_TOKEN_URL_QUERY_KEYS, parseSetupTokenCredential, parseSetupTokenPrompt, @@ -19,18 +20,35 @@ function readFixture(name: string): string { return readFileSync(path.join(fixturesDir, name), "utf8"); } -// A well-formed authorization URL that carries the exact query keys of the -// contract. The values are synthetic; the fixture redacts the real values. -const VALID_URL = - "https://claude.com/cai/oauth/authorize" + - "?client_id=cid-000" + - "&code=redacted" + - "&code_challenge=chal-000" + - "&code_challenge_method=S256" + - "&redirect_uri=https%3A%2F%2Fclaude.com%2Fcallback" + - "&response_type=code" + - "&scope=user%3Ainference" + - "&state=state-000"; +// The synthetic query values. Each value passes the value validators of the +// contract. No value is a real OAuth value. +const CLIENT_ID = "9d1c8f00-1a2b-3c4d-5e6f-708192a3b4c5"; +const CODE = "ac_0aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT"; +const CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"; +const STATE = "Xy7Kd2Pq9Rn4Vb8Lf1Mw6Zc3Hj0Tg5Us"; +const SCOPE = "org:create_api_key user:profile"; + +// The query of a well-formed authorization URL. The `redirect_uri` and the +// `scope` are percent-encoded the way the CLI emits them; the parser decodes and +// validates each value. +const QUERY = [ + `client_id=${CLIENT_ID}`, + `code=${CODE}`, + `code_challenge=${CODE_CHALLENGE}`, + "code_challenge_method=S256", + `redirect_uri=${encodeURIComponent(SETUP_TOKEN_REDIRECT_URI)}`, + "response_type=code", + `scope=${encodeURIComponent(SCOPE)}`, + `state=${STATE}`, +].join("&"); + +// The two observed authorization URLs. `claude` 2.1.19 emits the `claude.ai` +// URL; `claude` 2.1.205 and 2.1.226 emit the `claude.com` URL. +const CLAUDE_AI_URL = `https://claude.ai/oauth/authorize?${QUERY}`; +const CLAUDE_COM_URL = `https://claude.com/cai/oauth/authorize?${QUERY}`; + +// The default URL for the credential-parser regression tests below. +const VALID_URL = CLAUDE_COM_URL; // The URL preamble line and the browser-code prompt line, exactly as the // fixture records them. @@ -56,28 +74,66 @@ function osc8Hyperlink(url: string, display: string): string { return `${ESC}]8;;${url}${ESC}\\${display}${ESC}]8;;${ESC}\\`; } +// Joins `words` with a Cursor Horizontal Absolute (CHA) sequence. The live +// Claude login UI lays out each word at an absolute column with a CHA sequence +// instead of a literal space, so this reproduces one Ink-rendered line. The +// column number does not matter to the parser, so this uses a fixed number. The +// parser must render each CHA sequence as a space to read the line. +function chaSpaced(words: string[]): string { + return words.join("\x1b[9G"); +} + +// Wraps a string into physical lines of at most `width` characters, joined with +// a line feed and no space at the wrap. This reproduces the `claude` 2.1.19 hard +// line wrap of the plain-text authorization URL. +function hardWrap(text: string, width: number): string { + const lines: string[] = []; + for (let i = 0; i < text.length; i += width) { + lines.push(text.slice(i, i + width)); + } + return lines.join("\n"); +} + describe("parseSetupTokenPrompt", () => { - it("returns the exact URL and prompt from a complete output", () => { - const result = parseSetupTokenPrompt(completeOutput(VALID_URL)); + it("returns the exact claude.com URL and prompt from a complete output", () => { + const result = parseSetupTokenPrompt(completeOutput(CLAUDE_COM_URL)); expect(result).not.toBeNull(); - expect(result?.url).toBe(VALID_URL); + expect(result?.url).toBe(CLAUDE_COM_URL); + expect(result?.prompt).toBe(SETUP_TOKEN_PROMPT); + }); + + it("returns the exact claude.ai URL and prompt from a complete output", () => { + const result = parseSetupTokenPrompt(completeOutput(CLAUDE_AI_URL)); + expect(result).not.toBeNull(); + expect(result?.url).toBe(CLAUDE_AI_URL); + expect(result?.prompt).toBe(SETUP_TOKEN_PROMPT); + }); + + it("reassembles the claude.ai URL from the 2.1.19 hard-wrapped plain-text form", () => { + // `claude` 2.1.19 emits the URL as plain text with a hard line wrap and no + // OSC 8 hyperlink. The parser joins the URL-character-only physical lines and + // validates only the final reassembled string. + const wrapped = hardWrap(CLAUDE_AI_URL, 64); + expect(wrapped).toContain("\n"); + const text = [PREAMBLE_LINE, wrapped, PROMPT_LINE].join("\n"); + const result = parseSetupTokenPrompt(text); + expect(result).not.toBeNull(); + expect(result?.url).toBe(CLAUDE_AI_URL); expect(result?.prompt).toBe(SETUP_TOKEN_PROMPT); }); it("returns the URL when the output arrives split across chunks", () => { - // The streaming caller accumulates chunks and re-parses the whole buffer. - // The first chunk holds a partial URL, so the parser returns null. The joined - // buffer holds the whole prompt, so the parser returns the URL. - const chunkA = ["Opening browser to sign in…", PREAMBLE_LINE, "https://claude.com/cai/oauth/aut"].join( - "\n", - ); - const chunkB = ["horize?client_id=cid-000&code=redacted&code_challenge=chal-000&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fclaude.com%2Fcallback&response_type=code&scope=user%3Ainference&state=state-000", PROMPT_LINE, ""].join( + // The streaming caller accumulates chunks and re-parses the whole buffer. The + // chunk boundary falls inside the URL token with no line break, so the joined + // buffer holds the whole URL on one line. + const chunkA = ["Opening browser to sign in…", PREAMBLE_LINE, CLAUDE_COM_URL.slice(0, 40)].join( "\n", ); + const chunkB = [`${CLAUDE_COM_URL.slice(40)}`, PROMPT_LINE, ""].join("\n"); expect(parseSetupTokenPrompt(chunkA)).toBeNull(); const joined = parseSetupTokenPrompt(chunkA + chunkB); expect(joined).not.toBeNull(); - expect(joined?.url).toBe(VALID_URL); + expect(joined?.url).toBe(CLAUDE_COM_URL); expect(joined?.prompt).toBe(SETUP_TOKEN_PROMPT); }); @@ -86,48 +142,205 @@ describe("parseSetupTokenPrompt", () => { const reset = "\x1b[0m"; // The display text wraps across two lines and truncates the URL. The parser // must ignore it and read the URI field of the hyperlink. - const wrappedDisplay = "https://claude.com/cai/oauth/aut\nhorize?client_id=cid-000&code=…"; + const wrappedDisplay = "https://claude.com/cai/oauth/aut\nhorize?client_id=…"; const text = [ `${cyan}Opening browser to sign in…${reset}`, PREAMBLE_LINE, - `${cyan}${osc8Hyperlink(VALID_URL, wrappedDisplay)}${reset}`, + `${cyan}${osc8Hyperlink(CLAUDE_COM_URL, wrappedDisplay)}${reset}`, `${cyan}${PROMPT_LINE}${reset}`, ].join("\n"); const result = parseSetupTokenPrompt(text); expect(result).not.toBeNull(); - expect(result?.url).toBe(VALID_URL); + expect(result?.url).toBe(CLAUDE_COM_URL); expect(result?.prompt).toBe(SETUP_TOKEN_PROMPT); }); - it("returns null for a wrong origin", () => { - const badOrigin = VALID_URL.replace("https://claude.com", "https://claude.example.com"); - expect(parseSetupTokenPrompt(completeOutput(badOrigin))).toBeNull(); + it("returns null for a wrong host", () => { + const badHost = CLAUDE_COM_URL.replace("https://claude.com", "https://claude.example.com"); + expect(parseSetupTokenPrompt(completeOutput(badHost))).toBeNull(); }); - it("returns null for a wrong path", () => { - const badPath = VALID_URL.replace(SETUP_TOKEN_URL_PATH, "/cai/oauth/authorise"); - expect(parseSetupTokenPrompt(completeOutput(badPath))).toBeNull(); + it("returns null for a mixed-case host spelling", () => { + // The raw candidate must start with the exact lowercase host prefix, so a + // mixed-case host cannot bind even though the URL class lowercases the host. + const mixedCase = CLAUDE_AI_URL.replace("https://claude.ai", "https://Claude.AI"); + expect(parseSetupTokenPrompt(completeOutput(mixedCase))).toBeNull(); + }); + + it("returns null for an explicit default :443 port on the host", () => { + const withPort = CLAUDE_AI_URL.replace("https://claude.ai/", "https://claude.ai:443/"); + expect(parseSetupTokenPrompt(completeOutput(withPort))).toBeNull(); + }); + + it("returns null for a userinfo prefix on the host", () => { + const withUserinfo = CLAUDE_AI_URL.replace("https://claude.ai/", "https://user@claude.ai/"); + expect(parseSetupTokenPrompt(completeOutput(withUserinfo))).toBeNull(); + }); + + it("returns null for a punycode look-alike host", () => { + // A punycode host does not start with the exact lowercase prefix, so the + // parser rejects it before the URL parse. + const punycode = "https://xn--clade-9wa.ai/oauth/authorize?" + QUERY; + expect(parseSetupTokenPrompt(completeOutput(punycode))).toBeNull(); + }); + + it("returns null for the claude.ai host with the claude.com path", () => { + const crossPair = `https://claude.ai/cai/oauth/authorize?${QUERY}`; + expect(parseSetupTokenPrompt(completeOutput(crossPair))).toBeNull(); + }); + + it("returns null for the claude.com host with the claude.ai path", () => { + const crossPair = `https://claude.com/oauth/authorize?${QUERY}`; + expect(parseSetupTokenPrompt(completeOutput(crossPair))).toBeNull(); }); it("returns null for a missing query key", () => { - const missingKey = VALID_URL.replace("&state=state-000", ""); + const missingKey = CLAUDE_COM_URL.replace(`&state=${STATE}`, ""); expect(parseSetupTokenPrompt(completeOutput(missingKey))).toBeNull(); }); - it("returns null for an extra query key", () => { - const extraKey = `${VALID_URL}&extra=1`; - expect(parseSetupTokenPrompt(completeOutput(extraKey))).toBeNull(); + it("returns null for a duplicate query key", () => { + const duplicate = `${CLAUDE_COM_URL}&state=${STATE}`; + expect(parseSetupTokenPrompt(completeOutput(duplicate))).toBeNull(); + }); + + it("returns null for an unexpected added key with a valid name", () => { + // The parser accepts at most four added keys, so a first added key still + // needs a valid name; a valid name alone does not fail. This test proves a + // fifth added key fails the count bound. + const fiveAdded = `${CLAUDE_COM_URL}&a1=1&b2=2&c3=3&d4=4&e5=5`; + expect(parseSetupTokenPrompt(completeOutput(fiveAdded))).toBeNull(); + }); + + it("returns the URL when it carries up to four valid added keys", () => { + const fourAdded = `${CLAUDE_COM_URL}&a1=1&b2=2&c3=3&d4=4`; + const result = parseSetupTokenPrompt(completeOutput(fourAdded)); + expect(result).not.toBeNull(); + expect(result?.url).toBe(fourAdded); + }); + + it("returns null for an added key with an invalid name", () => { + const badName = `${CLAUDE_COM_URL}&Extra=1`; + expect(parseSetupTokenPrompt(completeOutput(badName))).toBeNull(); + }); + + it("returns null for an added key value over the length cap", () => { + const longValue = `${CLAUDE_COM_URL}&extra=${"a".repeat(257)}`; + expect(parseSetupTokenPrompt(completeOutput(longValue))).toBeNull(); + }); + + it("returns null for a duplicate added key", () => { + const dupAdded = `${CLAUDE_COM_URL}&extra=1&extra=2`; + expect(parseSetupTokenPrompt(completeOutput(dupAdded))).toBeNull(); + }); + + it("returns null for an invalid code_challenge_method", () => { + const badMethod = CLAUDE_COM_URL.replace("code_challenge_method=S256", "code_challenge_method=plain"); + expect(parseSetupTokenPrompt(completeOutput(badMethod))).toBeNull(); + }); + + it("returns null for an invalid response_type", () => { + const badType = CLAUDE_COM_URL.replace("response_type=code", "response_type=token"); + expect(parseSetupTokenPrompt(completeOutput(badType))).toBeNull(); + }); + + it("accepts a short four-character code value", () => { + // A real `claude` 2.1.19 capture measured a four-character `code` value, so + // the parser accepts it. + const shortCode = CLAUDE_AI_URL.replace(`code=${CODE}`, "code=wZ9x"); + const result = parseSetupTokenPrompt(completeOutput(shortCode)); + expect(result).not.toBeNull(); + expect(result?.url).toBe(shortCode); + }); + + it("returns null for an empty code value", () => { + const emptyCode = CLAUDE_COM_URL.replace(`code=${CODE}`, "code="); + expect(parseSetupTokenPrompt(completeOutput(emptyCode))).toBeNull(); + }); + + it("returns null for a code_challenge that is too short", () => { + const shortChallenge = CLAUDE_COM_URL.replace(`code_challenge=${CODE_CHALLENGE}`, "code_challenge=tooshort"); + expect(parseSetupTokenPrompt(completeOutput(shortChallenge))).toBeNull(); + }); + + it("returns null for a state that is too short", () => { + const shortState = CLAUDE_COM_URL.replace(`state=${STATE}`, "state=short"); + expect(parseSetupTokenPrompt(completeOutput(shortState))).toBeNull(); + }); + + it("returns null for a client_id with an invalid character", () => { + const badClientId = CLAUDE_COM_URL.replace(`client_id=${CLIENT_ID}`, "client_id=bad%2Fid"); + expect(parseSetupTokenPrompt(completeOutput(badClientId))).toBeNull(); + }); + + it("returns null for a scope with too many tokens", () => { + const scope = encodeURIComponent("a b c d e f g h i"); + const manyScope = CLAUDE_COM_URL.replace(`scope=${encodeURIComponent(SCOPE)}`, `scope=${scope}`); + expect(parseSetupTokenPrompt(completeOutput(manyScope))).toBeNull(); + }); + + it("returns null for a scope with an invalid token character", () => { + const scope = encodeURIComponent("user:profile bad/token"); + const badScope = CLAUDE_COM_URL.replace(`scope=${encodeURIComponent(SCOPE)}`, `scope=${scope}`); + expect(parseSetupTokenPrompt(completeOutput(badScope))).toBeNull(); + }); + + it("returns null for a redirect_uri with a wrong host", () => { + const badRedirect = encodeURIComponent("https://evil.example.com/oauth/code/callback"); + const url = CLAUDE_COM_URL.replace( + `redirect_uri=${encodeURIComponent(SETUP_TOKEN_REDIRECT_URI)}`, + `redirect_uri=${badRedirect}`, + ); + expect(parseSetupTokenPrompt(completeOutput(url))).toBeNull(); + }); + + it("returns null for a redirect_uri with a stray query", () => { + const badRedirect = encodeURIComponent(`${SETUP_TOKEN_REDIRECT_URI}?next=/`); + const url = CLAUDE_COM_URL.replace( + `redirect_uri=${encodeURIComponent(SETUP_TOKEN_REDIRECT_URI)}`, + `redirect_uri=${badRedirect}`, + ); + expect(parseSetupTokenPrompt(completeOutput(url))).toBeNull(); }); it("returns null for a URL with a fragment", () => { - const withFragment = `${VALID_URL}#section`; + const withFragment = `${CLAUDE_COM_URL}#section`; expect(parseSetupTokenPrompt(completeOutput(withFragment))).toBeNull(); }); + it("returns null for a raw sk-ant- value in a query value", () => { + const withKey = CLAUDE_COM_URL.replace(`code=${CODE}`, "code=sk-ant-oat01-abc12345"); + expect(parseSetupTokenPrompt(completeOutput(withKey))).toBeNull(); + }); + + it("returns null for a percent-encoded sk-ant- value in a query value", () => { + // The raw candidate hides the prefix behind `%2D`, so only the decoded value + // check catches it. + const withKey = CLAUDE_COM_URL.replace(`code=${CODE}`, "code=sk%2Dant%2Doat01%2Dabc12345"); + expect(parseSetupTokenPrompt(completeOutput(withKey))).toBeNull(); + }); + + it("returns null for a literal sk-ant- value in an added query key", () => { + // The secret rides inside a query name, not a value. The added-key name is + // valid in shape, so only the decoded-key check catches the prefix and the + // parser returns no accepted prompt result. + const withKey = `${CLAUDE_COM_URL}&sk-ant-redacted=x`; + expect(parseSetupTokenPrompt(completeOutput(withKey))).toBeNull(); + }); + + it("returns null for a percent-encoded sk-ant- value in an added query key", () => { + // The raw candidate hides the prefix behind `%2D`, so the raw-candidate + // check misses it. `URLSearchParams` decodes the name to `sk-ant-redacted`, + // so the decoded-key check catches it and the parser returns no accepted + // prompt result. + const withKey = `${CLAUDE_COM_URL}&sk%2Dant%2Dredacted=x`; + expect(parseSetupTokenPrompt(completeOutput(withKey))).toBeNull(); + }); + it("returns null when a code-like value sits on the line after the URL", () => { // The line right after the URL must hold the browser-code prompt. A code-like // value on that line cannot bind, so the parser returns null. - const text = [PREAMBLE_LINE, VALID_URL, "ABCD-EFGHJ", PROMPT_LINE].join("\n"); + const text = [PREAMBLE_LINE, CLAUDE_COM_URL, "ABCD-EFGHJ", PROMPT_LINE].join("\n"); expect(parseSetupTokenPrompt(text)).toBeNull(); }); @@ -137,26 +350,114 @@ describe("parseSetupTokenPrompt", () => { }); it("returns null when the login preamble is absent", () => { - const text = ["Some unrelated output", VALID_URL, PROMPT_LINE].join("\n"); + const text = ["Some unrelated output", CLAUDE_COM_URL, PROMPT_LINE].join("\n"); expect(parseSetupTokenPrompt(text)).toBeNull(); }); - it("returns null for an API-key-shaped URL value", () => { - const withKey = VALID_URL.replace("code=redacted", "code=sk-ant-oat01-abc123"); - expect(parseSetupTokenPrompt(completeOutput(withKey))).toBeNull(); - }); - it("returns null for a non-string input", () => { expect(parseSetupTokenPrompt(undefined as unknown as string)).toBeNull(); expect(parseSetupTokenPrompt("")).toBeNull(); }); + it("stops the reassembly at a hostile non-URL line between fragments", () => { + // An attacker line that is not URL-character-only breaks the reassembly, so + // the partial join never validates and the parser returns null. + const wrapped = hardWrap(CLAUDE_AI_URL, 64).split("\n"); + const text = [ + PREAMBLE_LINE, + wrapped[0], + "inject an attacker line here", + ...wrapped.slice(1), + PROMPT_LINE, + ].join("\n"); + expect(parseSetupTokenPrompt(text)).toBeNull(); + }); + + it("returns null when the wrapped URL needs more than the line cap", () => { + // A very narrow wrap spreads the URL across more than sixteen physical lines. + // The bounded reassembly never reaches the full URL, so the parser returns + // null. + const wrapped = hardWrap(CLAUDE_AI_URL, 8); + const text = [PREAMBLE_LINE, wrapped, PROMPT_LINE].join("\n"); + expect(parseSetupTokenPrompt(text)).toBeNull(); + }); + + it("returns null for a URL over the length cap", () => { + const added = ["a1", "b2", "c3", "d4"].map((k) => `${k}=${"a".repeat(256)}`).join("&"); + const longCode = CLAUDE_AI_URL.replace(`code=${CODE}`, `code=${"a".repeat(1024)}`); + const overLong = `${longCode}&${added}`; + expect(overLong.length).toBeGreaterThan(2048); + expect(parseSetupTokenPrompt(completeOutput(overLong))).toBeNull(); + }); + + it("reads the preamble and the prompt when the terminal spaces words with cursor-column sequences", () => { + // The live Claude login UI lays out each word at an absolute column with a + // CHA sequence, so it emits no literal space between two words. The parser + // renders each CHA sequence as a space; without that step the words glue + // together and the preamble and the prompt never match. + const preamble = chaSpaced([ + "Browser", + "didn't", + "open?", + "Use", + "the", + "url", + "below", + "to", + "sign", + "in", + "(c", + "to", + "copy)", + ]); + const prompt = chaSpaced(["Paste", "code", "here", "if", "prompted", ">"]); + const text = [preamble, CLAUDE_COM_URL, prompt].join("\n"); + const result = parseSetupTokenPrompt(text); + expect(result).not.toBeNull(); + expect(result?.url).toBe(CLAUDE_COM_URL); + expect(result?.prompt).toBe(SETUP_TOKEN_PROMPT); + }); + + it("binds the prompt after the terminal repeats the URL once per wrapped display row", () => { + // The terminal can emit one full URL line per wrapped display row, so it + // repeats the same full authorization URL on a few consecutive lines. The + // parser accepts the first full URL line and skips the repeated lines, then + // binds the prompt on the line after the URL block. + const text = [ + "Browser didn't open? Use the url below to sign in (c to copy)", + CLAUDE_COM_URL, + CLAUDE_COM_URL, + CLAUDE_COM_URL, + "", + "Paste code here if prompted >", + ].join("\n"); + const result = parseSetupTokenPrompt(text); + expect(result).not.toBeNull(); + expect(result?.url).toBe(CLAUDE_COM_URL); + expect(result?.prompt).toBe(SETUP_TOKEN_PROMPT); + }); + + it("returns null when an unrelated line sits after the URL block", () => { + // A non-blank line that is neither the prompt nor a repeat of the URL breaks + // the bind, so a stray value between the URL and the prompt cannot pass. + const text = [ + "Browser didn't open? Use the url below to sign in (c to copy)", + CLAUDE_COM_URL, + CLAUDE_COM_URL, + "an unrelated line", + "Paste code here if prompted >", + ].join("\n"); + expect(parseSetupTokenPrompt(text)).toBeNull(); + }); + it("keeps its contract in sync with the characterization fixture", () => { - // The fixture documents the prompt text, the URL path, and the query keys. - // This test fails if the parser contract drifts from the fixture. + // The fixture documents the prompt text, the two authorization URLs, and the + // query keys. This test fails if the parser contract drifts from the fixture. const fixture = readFixture("setup-token.md"); expect(fixture).toContain(SETUP_TOKEN_PROMPT); - expect(fixture).toContain(SETUP_TOKEN_URL_PATH); + for (const url of SETUP_TOKEN_AUTH_URLS) { + expect(fixture).toContain(url); + } for (const key of SETUP_TOKEN_URL_QUERY_KEYS) { expect(fixture).toContain(`\`${key}\``); } @@ -171,9 +472,12 @@ const TOKEN_FRAGMENT_A = `${SETUP_TOKEN_PREFIX}AAAABBBBCCCCDDDDEEEE1111`; const TOKEN_FRAGMENT_B = "2222FFFFGGGG_HHHH-IIII"; const FULL_TOKEN = `${TOKEN_FRAGMENT_A}${TOKEN_FRAGMENT_B}`; -// Builds the success screen around a token body. The body holds the token -// fragment lines, already split the way the terminal wraps them. -function successScreen(body: string[]): string { +// Builds the success screen around a token body, joined with `delimiter`. The +// body holds the token fragment lines, already split the way the terminal wraps +// them. The real terminal redraw joins the anchor lines and the token with a +// bare carriage return, or a CRLF pair, instead of a line feed, so the tests +// vary the delimiter to prove the parser reads each real record shape. +function successScreenJoined(body: string[], delimiter: string): string { return [ "✓ Long-lived authentication token created successfully!", "", @@ -184,7 +488,12 @@ function successScreen(body: string[]): string { SETUP_TOKEN_AFTER_ANCHOR, "", "Use this token by setting: export CLAUDE_CODE_OAUTH_TOKEN=", - ].join("\n"); + ].join(delimiter); +} + +// Builds the success screen around a token body, joined with line feeds. +function successScreen(body: string[]): string { + return successScreenJoined(body, "\n"); } describe("parseSetupTokenCredential", () => { @@ -198,6 +507,58 @@ describe("parseSetupTokenCredential", () => { expect(parseSetupTokenCredential(text)).toBe(FULL_TOKEN); }); + it("returns the token from a bare carriage-return success record", () => { + // The real terminal redraw joins the before-anchor line, the token, and the + // after-anchor line with a bare carriage return. The parser canonicalizes + // each bare carriage return to a line feed before the split, so each anchor + // line and the token land on their own lines and the token binds. + const text = successScreenJoined([FULL_TOKEN], "\r"); + expect(parseSetupTokenCredential(text)).toBe(FULL_TOKEN); + }); + + it("returns the token from a CRLF-delimited success record", () => { + // A terminal that ends each line with a carriage return and a line feed + // must parse the same as a plain line-feed record. The parser maps each + // CRLF pair to one line feed, so no empty line splits between the anchors. + const text = successScreenJoined([FULL_TOKEN], "\r\n"); + expect(parseSetupTokenCredential(text)).toBe(FULL_TOKEN); + }); + + it("returns the de-wrapped token from a bare carriage-return wrapped record", () => { + // The terminal wraps the token across two physical lines and joins every + // line with a bare carriage return. The parser canonicalizes the delimiter, + // then joins the two fragment lines into the full token. + const text = successScreenJoined([TOKEN_FRAGMENT_A, TOKEN_FRAGMENT_B], "\r"); + expect(parseSetupTokenCredential(text)).toBe(FULL_TOKEN); + }); + + it("returns null for duplicate anchors in a bare carriage-return record", () => { + // Canonicalization keeps the duplicate-anchor guard closed. Two success + // blocks joined by a bare carriage return still fail closed. + const one = successScreenJoined([FULL_TOKEN], "\r"); + const text = `${one}\r${one}`; + expect(parseSetupTokenCredential(text)).toBeNull(); + }); + + it("returns null for junk between the anchors in a bare carriage-return record", () => { + // A stray prose line between the anchors fails the fragment test, so the + // parser fails closed after canonicalization. + const text = successScreenJoined([TOKEN_FRAGMENT_A, "a stray note", TOKEN_FRAGMENT_B], "\r"); + expect(parseSetupTokenCredential(text)).toBeNull(); + }); + + it("returns null for a wrong token prefix in a bare carriage-return record", () => { + const text = successScreenJoined(["sk-ant-api03-AAAABBBBCCCCDDDDEEEE1111"], "\r"); + expect(parseSetupTokenCredential(text)).toBeNull(); + }); + + it("returns null for extra token text in a bare carriage-return record", () => { + // The token line holds the token and extra prose. The parser reads only a + // token fragment on each line, so canonicalization does not open a gap. + const text = successScreenJoined([`${FULL_TOKEN} keep this secret`], "\r"); + expect(parseSetupTokenCredential(text)).toBeNull(); + }); + it("reads the token through ANSI color sequences", () => { const cyan = "\x1b[36m"; const reset = "\x1b[0m"; @@ -205,6 +566,38 @@ describe("parseSetupTokenCredential", () => { expect(parseSetupTokenCredential(text)).toBe(FULL_TOKEN); }); + it("reads the token when the terminal spaces the anchor words with cursor-column sequences", () => { + // The success screen renders the anchor lines the same way as the prompt: it + // lays out each word at an absolute column with a CHA sequence. The parser + // renders each CHA sequence as a space, so the anchor lines match the exact + // anchor text and the token binds between them. + const beforeAnchor = chaSpaced([ + "Your", + "OAuth", + "token", + "(valid", + "for", + "1", + "year):", + ]); + const afterAnchor = chaSpaced([ + "Store", + "this", + "token", + "securely.", + "You", + "won't", + "be", + "able", + "to", + "see", + "it", + "again.", + ]); + const text = [beforeAnchor, "", TOKEN_FRAGMENT_A, TOKEN_FRAGMENT_B, "", afterAnchor].join("\n"); + expect(parseSetupTokenCredential(text)).toBe(FULL_TOKEN); + }); + it("returns null for a token-shaped value before submit", () => { // The prompt screen holds a token-shaped value but no success anchors. The // parser fails closed, so an early value never delivers. diff --git a/packages/adapters/claude-local/src/server/setup-token-parse.ts b/packages/adapters/claude-local/src/server/setup-token-parse.ts index 0ad6b14bd5..d845f5ab34 100644 --- a/packages/adapters/claude-local/src/server/setup-token-parse.ts +++ b/packages/adapters/claude-local/src/server/setup-token-parse.ts @@ -3,12 +3,19 @@ // browser-code prompt, or null. `parseSetupTokenCredential` returns the minted // OAuth token from the success screen, or null. // -// Security (strict validation): the prompt parser accepts only the exact origin -// and path of the authorization URL, and only the exact set of query keys. It -// rejects any other origin, path, query, or fragment. It binds the browser-code -// prompt to a dedicated line right after the URL line, so an unrelated code-like -// value on the wrong line cannot form a prompt. It rejects an API-key-shaped -// value inside the URL. +// Security (shape validation): the prompt parser accepts an authorization URL +// only when it matches one of the two observed origin-and-path pairs, carries +// each required query key one time with a valid decoded value, and pins the +// `redirect_uri` value to the one observed callback. It rejects a look-alike +// host, userinfo, an explicit port, a fragment, a duplicate key, an unexpected +// added key, an invalid value, and an API-key-shaped value in the raw URL or in +// any decoded query value. Different Claude CLI versions render the URL two ways: +// a single OSC 8 hyperlink, or plain text with a hard line wrap and no +// hyperlink. The parser reassembles the wrapped plain-text URL from the physical +// lines and validates only the final reassembled string, so the login works on +// every CLI version that keeps this flow shape. It binds the browser-code prompt +// to the dedicated line after the URL block; a repeated URL line, a code-like +// value on the wrong line, or an unrelated line cannot form a prompt. // // The token parser binds the token to the exact success record: it requires the // before-anchor line and the after-anchor line, in that order, exactly one time @@ -30,16 +37,36 @@ export interface SetupTokenPrompt { // exact constant on a match. export const SETUP_TOKEN_PROMPT = "Paste code here if prompted"; -// The one and only accepted authorization URL origin. -export const SETUP_TOKEN_URL_ORIGIN = "https://claude.com"; +// The two accepted authorization URLs, each the exact origin plus path. The +// parser accepts an origin-and-path pair only when it equals one of these two +// strings. It does not accept the cross-product of the two hosts and the two +// paths. `claude` 2.1.19 emits the first URL; `claude` 2.1.205 and 2.1.226 emit +// the second URL. +export const SETUP_TOKEN_AUTH_URLS = [ + "https://claude.ai/oauth/authorize", + "https://claude.com/cai/oauth/authorize", +] as const; -// The one and only accepted authorization URL path. -export const SETUP_TOKEN_URL_PATH = "/cai/oauth/authorize"; +// The lowercase host prefix of each accepted authorization URL. The parser +// requires the raw URL candidate to start with one of these exact lowercase +// prefixes before it parses the URL. This rejects a mixed-case host spelling, a +// punycode host, and an explicit default `:443` port, because none of them start +// with the exact lowercase `https:///` prefix. +const SETUP_TOKEN_AUTH_URL_PREFIXES = [ + "https://claude.ai/", + "https://claude.com/", +] as const; -// The exact set of query keys of the authorization URL. The parser accepts the -// URL only when its query keys equal this set: no missing key, no extra key, and -// no duplicate key. The parser does not validate the query values, because the -// values are the real OAuth PKCE parameters that the user must visit. +// The one accepted `redirect_uri` value. The parser parses the decoded +// `redirect_uri` value on its own and pins it to this exact callback, with no +// userinfo, no port, no query, and no fragment. The 2.1.226 live capture grounds +// this host; the contract keeps a single pin until sanitized 2.1.19 evidence +// proves another real callback. +export const SETUP_TOKEN_REDIRECT_URI = "https://platform.claude.com/oauth/code/callback"; + +// The required query keys of the authorization URL. The parser requires each key +// one time with a valid decoded value. It rejects a missing key, a duplicate +// key, and an invalid value. export const SETUP_TOKEN_URL_QUERY_KEYS = [ "client_id", "code", @@ -51,6 +78,55 @@ export const SETUP_TOKEN_URL_QUERY_KEYS = [ "state", ] as const; +// The set form of the required query keys. The parser uses it to sort a key into +// a required key or an added key. +const SETUP_TOKEN_CORE_KEY_SET = new Set(SETUP_TOKEN_URL_QUERY_KEYS); + +// The value validators for the required query keys. Each pattern binds the whole +// decoded value. The parser rejects a value that does not match. +const CLIENT_ID_RE = /^[A-Za-z0-9._~-]{1,128}$/; +// The `code` value of the `claude` 2.1.19 authorization request is a short +// opaque token. A real 2.1.19 capture measured a 4-character value, so the +// minimum length is one character. The character class and the 1024-character +// maximum bound the value. +const CODE_RE = /^[A-Za-z0-9._~-]{1,1024}$/; +const CODE_CHALLENGE_RE = /^[A-Za-z0-9_-]{43,128}$/; +const STATE_RE = /^[A-Za-z0-9_-]{16,256}$/; +// One `scope` token. The `scope` value is one to eight space-separated tokens. +const SCOPE_TOKEN_RE = /^[A-Za-z0-9:._-]{1,64}$/; + +// The name of an added query key. An added key is any key that is not a required +// key. The parser accepts at most four added keys, each with a name that matches +// this pattern. +const ADDED_KEY_NAME_RE = /^[a-z][a-z0-9_-]{0,63}$/; + +// The decoded value of an added query key. The parser accepts at most 256 +// printable ASCII characters. +const ADDED_VALUE_RE = /^[\x20-\x7E]{0,256}$/; + +// An API-key prefix. The Claude setup token and API key start with `sk-ant-`. +// The parser rejects the raw URL candidate and any decoded query value that +// holds this prefix, so a secret cannot ride inside the authorization URL. +const API_KEY_RE = /sk-ant-/i; + +// The maximum number of added query keys. The parser accepts at most four added +// keys, so the URL holds at most twelve keys in total. +const MAX_ADDED_KEYS = 4; + +// The maximum number of `scope` tokens. +const MAX_SCOPE_TOKENS = 8; + +// The maximum length of the whole `scope` value. +const MAX_SCOPE_LENGTH = 512; + +// The maximum length of the reassembled authorization URL. The parser rejects a +// candidate longer than this cap. +const MAX_URL_LENGTH = 2048; + +// The characters that a URL can hold. A physical line that reassembles into the +// wrapped URL holds only these characters after a trim. +const URL_CHARS_ONLY_RE = /^[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+$/; + // An ANSI Control Sequence Introducer (CSI): the ESC control byte, a `[`, zero // or more parameter bytes (0x30-0x3F), zero or more intermediate bytes // (0x20-0x2F), and one final byte (0x40-0x7E). The Claude terminal wraps the @@ -59,6 +135,18 @@ export const SETUP_TOKEN_URL_QUERY_KEYS = [ // eslint-disable-next-line no-control-regex const ANSI_CSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g; +// A horizontal-move CSI sequence: Cursor Horizontal Absolute (final byte `G`) +// and Cursor Forward (final byte `C`). The Claude login UI lays out each word at +// an absolute column with one of these sequences, so it emits no literal space +// between two words. The parser replaces each horizontal-move sequence with one +// space, so a word gap reads as a space. Without this step the later CSI removal +// deletes the sequence and glues the two words, and the URL preamble line, the +// browser-code prompt line, and the success anchor lines never match. The +// vertical and color sequences carry no horizontal text gap; the CSI removal +// below drops them. +// eslint-disable-next-line no-control-regex +const CSI_HORIZONTAL_MOVE_RE = /\x1b\[[0-9;]*[GC]/g; + // An OSC 8 hyperlink. The terminal emits the URL through an OSC 8 hyperlink plus // wrapped display text. The hyperlink carries the true, unwrapped URL in its URI // field, while the display text may wrap across lines. The parser replaces the @@ -76,14 +164,10 @@ const OSC8_HYPERLINK_RE = // eslint-disable-next-line no-control-regex const OSC_ANY_RE = /\x1b\][^\x1b\x07]*(?:\x1b\\|\x07)/g; -// A candidate URL token: a run of non-space characters that starts with an http -// or https scheme. The parser validates each candidate with the `URL` class; the -// regular expression only splits tokens out of the text. -const URL_TOKEN_RE = /https?:\/\/\S+/g; - // Trailing punctuation that prose commonly puts right after a URL. The parser -// strips only these characters. It never strips `?` or `#`, so a URL with a -// stray query or a fragment stays malformed and the parser rejects it. +// strips only these characters when it compares a line to a repeated URL. It +// never strips these from the candidate before validation, because the validator +// rejects any trailing punctuation on its own. const TRAILING_PUNCTUATION_RE = /[)\].,;:!]+$/; // The line that introduces the URL. The Claude login UI prints this phrase right @@ -97,134 +181,218 @@ const URL_PREAMBLE_RE = /the url below to sign in/i; // the line. const PROMPT_LINE_RE = /^Paste code here if prompted\b[\s>*]*$/; -// An API-key-shaped value. The Claude setup token and API key start with the -// `sk-ant-` prefix. The parser rejects a URL that embeds such a value, so a -// secret cannot ride inside the authorization URL. -const API_KEY_RE = /sk-ant-[a-z0-9-]{2,}/i; - // The maximum number of characters between the end of the URL preamble line and // the start of the URL. The Claude UI prints the URL right after the preamble. -// The parser looks for the URL only inside this window, so a URL far from the +// The parser accepts a URL start only inside this window, so a URL far from the // preamble cannot bind. const MAX_PREAMBLE_TO_URL_GAP = 512; -// The maximum number of characters after the end of the URL line that the parser -// reads for the browser-code prompt. The Claude UI prints the prompt one line -// after the URL. The window is large enough for the real prompt line and small -// enough to reject distant noise. -const MAX_URL_TO_PROMPT_GAP = 256; +// The maximum number of consecutive physical lines that the parser joins to +// reassemble a wrapped plain-text URL. The `claude` 2.1.19 CLI wraps the URL at +// the terminal width with no space at the wrap, so the parser joins a bounded +// run of URL-character-only lines. +const MAX_URL_LINES = 16; -// The result of a URL search: the validated URL and the index of the first -// character after the matched URL token in the search window. +// The result of a URL search: the validated URL and the index of the physical +// line that holds the last fragment of the URL. interface SetupTokenUrlMatch { url: string; - end: number; + endLine: number; } /** * Removes the terminal control sequences from `text`. Replaces each OSC 8 * hyperlink with its true URI, so the parser reads the unwrapped URL and drops - * the wrapped display text. Removes each leftover OSC sequence and each ANSI CSI - * sequence. Returns plain text. The function only normalizes the input; the URL - * and the prompt still pass the strict validation below. + * the wrapped display text. Removes each leftover OSC sequence. Replaces each + * horizontal-move CSI sequence with one space, so a word that the login UI lays + * out at an absolute column reads as a space-separated word. Removes each + * remaining ANSI CSI sequence. Returns plain text. The function only normalizes + * the input; the URL and the prompt still pass the shape validation below. */ function stripTerminalControls(text: string): string { return text .replace(OSC8_HYPERLINK_RE, "$1") .replace(OSC_ANY_RE, "") + .replace(CSI_HORIZONTAL_MOVE_RE, " ") .replace(ANSI_CSI_RE, ""); } /** - * Returns the slice of `text` that starts at `start` and holds at most - * `maxLength` characters. The parser uses this window to bound a gap between two - * anchors, so a distant match cannot bind. + * Returns true when `value` is a valid `scope` value: one to eight + * space-separated tokens, a total length of at most {@link MAX_SCOPE_LENGTH}, + * and each token a match of {@link SCOPE_TOKEN_RE}. A double space forms an empty + * token, which fails the token pattern, so a malformed value fails closed. */ -function gapWindow(text: string, start: number, maxLength: number): string { - return text.slice(start, start + maxLength); +function isValidScope(value: string): boolean { + if (value.length < 1 || value.length > MAX_SCOPE_LENGTH) return false; + const tokens = value.split(" "); + if (tokens.length < 1 || tokens.length > MAX_SCOPE_TOKENS) return false; + return tokens.every((token) => SCOPE_TOKEN_RE.test(token)); } /** - * Returns true when the query keys of `parsed` equal - * {@link SETUP_TOKEN_URL_QUERY_KEYS} exactly: no missing key, no extra key, and - * no duplicate key. + * Returns true when `value` is the one accepted `redirect_uri`. The function + * parses the decoded value on its own and pins it to {@link + * SETUP_TOKEN_REDIRECT_URI}, with the `https:` protocol, no userinfo, no port, + * no query, and no fragment. It never broadens to an arbitrary HTTPS URL. */ -function hasExactQueryKeys(parsed: URL): boolean { - const keys = [...parsed.searchParams.keys()]; - if (keys.length !== SETUP_TOKEN_URL_QUERY_KEYS.length) return false; - const seen = new Set(keys); - if (seen.size !== keys.length) return false; - return SETUP_TOKEN_URL_QUERY_KEYS.every((key) => seen.has(key)); +function isValidRedirectUri(value: string): boolean { + let parsed: URL; + try { + parsed = new URL(value); + } catch { + return false; + } + if (parsed.protocol !== "https:") return false; + if (parsed.username !== "" || parsed.password !== "") return false; + if (parsed.port !== "") return false; + if (parsed.search !== "" || parsed.hash !== "") return false; + return `${parsed.origin}${parsed.pathname}` === SETUP_TOKEN_REDIRECT_URI; } /** - * Returns the authorization URL and its end index when `window` holds it with - * the exact origin {@link SETUP_TOKEN_URL_ORIGIN}, the exact path - * {@link SETUP_TOKEN_URL_PATH}, the exact query keys, no fragment, and no - * credentials. Rejects a URL that embeds an API-key-shaped value. Returns null - * otherwise. The `end` index is the position of the first character after the - * matched token in `window`. + * Returns true when the query of `parsed` matches the contract. It requires each + * required key one time with a valid decoded value. It rejects a duplicate for + * any key. It accepts at most {@link MAX_ADDED_KEYS} added keys, each with a + * valid name, one value, and a decoded value of at most 256 printable ASCII + * characters. It rejects an API-key prefix in any decoded value. */ -function findExactUrl(window: string): SetupTokenUrlMatch | null { - for (const match of window.matchAll(URL_TOKEN_RE)) { - const token = match[0]; - if (API_KEY_RE.test(token)) continue; - const cleaned = token.replace(TRAILING_PUNCTUATION_RE, ""); - let parsed: URL; - try { - parsed = new URL(cleaned); - } catch { - continue; - } - if ( - parsed.protocol === "https:" && - parsed.host === "claude.com" && - parsed.pathname === SETUP_TOKEN_URL_PATH && - parsed.hash === "" && - parsed.username === "" && - parsed.password === "" && - hasExactQueryKeys(parsed) - ) { - return { url: cleaned, end: (match.index ?? 0) + token.length }; - } +function hasValidQuery(parsed: URL): boolean { + // Collect the values of each key, so the parser can reject a duplicate key and + // read the one decoded value of each key. + const byKey = new Map(); + for (const [key, value] of parsed.searchParams) { + const values = byKey.get(key); + if (values) values.push(value); + else byKey.set(key, [value]); + } + + // Reject a duplicate for any key. After this test each key holds one value. + for (const values of byKey.values()) { + if (values.length !== 1) return false; + } + + // Reject an API-key prefix in any decoded key and any decoded value. The + // `URLSearchParams` decoder turns a percent-encoded name into its literal + // form, so a secret-shaped value can hide in a query name (for example, + // `sk%2Dant%2Dredacted` decodes to `sk-ant-redacted`). The parser rejects the + // prefix in the name and in the value, before it classifies an added key. The + // raw-candidate check stays in place as defense in depth. + for (const [key, values] of byKey) { + if (API_KEY_RE.test(key)) return false; + if (API_KEY_RE.test(values[0])) return false; + } + + // Require each required key. + for (const key of SETUP_TOKEN_URL_QUERY_KEYS) { + if (!byKey.has(key)) return false; + } + + // Bound the added keys and validate each one. + const addedKeys = [...byKey.keys()].filter((key) => !SETUP_TOKEN_CORE_KEY_SET.has(key)); + if (addedKeys.length > MAX_ADDED_KEYS) return false; + for (const key of addedKeys) { + if (!ADDED_KEY_NAME_RE.test(key)) return false; + if (!ADDED_VALUE_RE.test(byKey.get(key)![0])) return false; + } + + // Validate each required value. + if (!CLIENT_ID_RE.test(byKey.get("client_id")![0])) return false; + if (!CODE_RE.test(byKey.get("code")![0])) return false; + if (!CODE_CHALLENGE_RE.test(byKey.get("code_challenge")![0])) return false; + if (byKey.get("code_challenge_method")![0] !== "S256") return false; + if (byKey.get("response_type")![0] !== "code") return false; + if (!STATE_RE.test(byKey.get("state")![0])) return false; + if (!isValidScope(byKey.get("scope")![0])) return false; + if (!isValidRedirectUri(byKey.get("redirect_uri")![0])) return false; + return true; +} + +/** + * Returns `candidate` when it is a valid authorization URL, or null. The final + * validator applies to every candidate, from an OSC 8 URI or from a plain-text + * reassembly. It requires the length cap {@link MAX_URL_LENGTH}, an exact + * lowercase host prefix, no API-key prefix in the raw candidate, the `https:` + * protocol, empty userinfo, empty port, no fragment, one of the two accepted + * origin-and-path pairs, and a valid query. + */ +function validateAuthUrl(candidate: string): string | null { + if (candidate.length > MAX_URL_LENGTH) return null; + if (!SETUP_TOKEN_AUTH_URL_PREFIXES.some((prefix) => candidate.startsWith(prefix))) return null; + if (API_KEY_RE.test(candidate)) return null; + let parsed: URL; + try { + parsed = new URL(candidate); + } catch { + return null; + } + if (parsed.protocol !== "https:") return null; + if (parsed.username !== "" || parsed.password !== "") return null; + if (parsed.port !== "" || parsed.hash !== "") return null; + const pair = `${parsed.origin}${parsed.pathname}`; + if (!SETUP_TOKEN_AUTH_URLS.includes(pair as (typeof SETUP_TOKEN_AUTH_URLS)[number])) return null; + if (!hasValidQuery(parsed)) return null; + return candidate; +} + +/** + * Reassembles a wrapped plain-text URL from the physical lines and validates it. + * The parser starts at `startLine`, then joins at most {@link MAX_URL_LINES} + * consecutive URL-character-only physical lines into at most {@link + * MAX_URL_LENGTH} characters. It validates the joined string after each join and + * returns the first joined string that passes {@link validateAuthUrl}. A CLI that + * emits the whole URL on one line passes on the first join. A CLI that wraps the + * URL passes once the parser joins the last fragment. Returns null when no bounded + * join validates. + */ +function reassembleUrl(lines: string[], startLine: number): SetupTokenUrlMatch | null { + let joined = ""; + for (let offset = 0; offset < MAX_URL_LINES && startLine + offset < lines.length; offset += 1) { + const trimmed = lines[startLine + offset].trim(); + if (trimmed.length === 0 || !URL_CHARS_ONLY_RE.test(trimmed)) break; + joined += trimmed; + if (joined.length > MAX_URL_LENGTH) break; + const url = validateAuthUrl(joined); + if (url) return { url, endLine: startLine + offset }; } return null; } /** - * Returns the canonical browser-code prompt when `text` holds it on a dedicated - * line after the URL line. The parser advances past the URL line to the first - * newline, then reads the first non-blank line inside a window after it. That - * line must match the exact prompt shape. So the prompt binds to the dedicated - * line that the UI prints right after the URL. A code-like value on that line, or - * the prompt phrase mixed with other prose, cannot bind. Returns null when the - * URL line has no line break after it, or when the first non-blank line is not - * the prompt. + * Returns the canonical browser-code prompt when `lines` holds it on a dedicated + * line after the URL block. The parser reads the non-blank lines after the URL + * end line. The Claude UI can emit one full URL line per wrapped display row, so + * it repeats the same full authorization URL on a few consecutive lines. The + * parser skips a line that repeats `url`, then binds the prompt on the first + * non-blank, non-repeat line. That line must match the exact prompt shape. + * Returns null when the first non-blank, non-repeat line is not the prompt. */ -function findBrowserCodePrompt(text: string, urlEnd: number): string | null { - const lineBreak = text.indexOf("\n", urlEnd); - if (lineBreak === -1) return null; - const window = gapWindow(text, lineBreak + 1, MAX_URL_TO_PROMPT_GAP); - for (const line of window.split("\n")) { - const trimmed = line.trim(); +function findBrowserCodePrompt(lines: string[], endLine: number, url: string): string | null { + for (let i = endLine + 1; i < lines.length; i += 1) { + const trimmed = lines[i].trim(); if (trimmed.length === 0) continue; + // Skip a line that repeats the full URL. Strip the trailing prose + // punctuation first, the same way the repeated-URL comparison needs, so a + // repeated URL with a trailing punctuation mark still matches. + if (trimmed.replace(TRAILING_PUNCTUATION_RE, "") === url) continue; return PROMPT_LINE_RE.test(trimmed) ? SETUP_TOKEN_PROMPT : null; } return null; } /** - * Parses Claude `setup-token` login output. Removes ANSI CSI sequences and OSC 8 - * hyperlink sequences first, so colored and hyperlinked output reads the same as - * plain output. The parser finds the URL preamble, then the exact authorization - * URL inside a window after the preamble, then the browser-code prompt on the - * dedicated line after the URL line. So the URL binds to the login context and - * the prompt binds to the URL. Returns the authorization URL and the browser-code - * prompt when both are present and valid. Returns null for any other input, - * including a non-string input, an absent preamble, a URL with a wrong origin, - * path, query, or fragment, an API-key-shaped URL, and a missing or misplaced - * prompt. Never throws on input, and never puts any input byte into a log or an - * error. This phase returns no token. + * Parses Claude `setup-token` login output. Removes the terminal control + * sequences first, so colored and hyperlinked output reads the same as plain + * output. The parser finds the URL preamble, then a physical line that starts the + * authorization URL within {@link MAX_PREAMBLE_TO_URL_GAP} characters of the + * preamble, then reassembles the wrapped URL and validates the final string, then + * binds the browser-code prompt on the dedicated line after the URL block. So the + * URL binds to the login context and the prompt binds to the URL. Returns the + * authorization URL and the browser-code prompt when both are present and valid. + * Returns null for any other input, including a non-string input, an absent + * preamble, a URL with a wrong host, path, query, or fragment, an API-key-shaped + * URL, and a missing or misplaced prompt. Never throws on input, and never puts + * any input byte into a log or an error. This phase returns no token. */ export function parseSetupTokenPrompt(text: string): SetupTokenPrompt | null { if (typeof text !== "string" || text.length === 0) return null; @@ -232,13 +400,25 @@ export function parseSetupTokenPrompt(text: string): SetupTokenPrompt | null { const preamble = URL_PREAMBLE_RE.exec(clean); if (!preamble) return null; const afterPreamble = preamble.index + preamble[0].length; - const urlWindow = gapWindow(clean, afterPreamble, MAX_PREAMBLE_TO_URL_GAP); - const urlMatch = findExactUrl(urlWindow); - if (!urlMatch) return null; - const urlEnd = afterPreamble + urlMatch.end; - const prompt = findBrowserCodePrompt(clean, urlEnd); - if (!prompt) return null; - return { url: urlMatch.url, prompt }; + // The region after the preamble. The URL must start inside the start window. + const region = clean.slice(afterPreamble); + const lines = region.split("\n"); + + // Walk the physical lines and track the character offset of each line start. + // The URL must start within MAX_PREAMBLE_TO_URL_GAP characters of the preamble. + let offset = 0; + for (let i = 0; i < lines.length; i += 1) { + const lineStart = offset; + offset += lines[i].length + 1; + if (lineStart > MAX_PREAMBLE_TO_URL_GAP) break; + const trimmed = lines[i].trim(); + if (!SETUP_TOKEN_AUTH_URL_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) continue; + const match = reassembleUrl(lines, i); + if (!match) continue; + const prompt = findBrowserCodePrompt(lines, match.endLine, match.url); + if (prompt) return { url: match.url, prompt }; + } + return null; } // --- The success token parser ------------------------------------------------ @@ -305,7 +485,15 @@ function singleAnchorIndex(lines: string[], anchor: string): number | null { export function parseSetupTokenCredential(text: string): string | null { if (typeof text !== "string" || text.length === 0) return null; const clean = stripTerminalControls(text); - const lines = clean.split("\n"); + // Canonicalize the record delimiter. The real terminal redraw joins the + // before-anchor line, the token, and the after-anchor line with a bare + // carriage return, or a CRLF pair, instead of a line feed. Map each CRLF and + // each bare carriage return to one line feed, so each anchor line and each + // token fragment lands on its own line. This step changes the record + // delimiter only. It cannot add a token byte, and the interval validation + // below still rejects partial, extra, or prose data. + const normalized = clean.replace(/\r\n|\r/g, "\n"); + const lines = normalized.split("\n"); const beforeIndex = singleAnchorIndex(lines, SETUP_TOKEN_BEFORE_ANCHOR); const afterIndex = singleAnchorIndex(lines, SETUP_TOKEN_AFTER_ANCHOR); diff --git a/packages/adapters/claude-local/src/server/setup-token-runner.test.ts b/packages/adapters/claude-local/src/server/setup-token-runner.test.ts index 7e4cd2db2e..ede26a52a7 100644 --- a/packages/adapters/claude-local/src/server/setup-token-runner.test.ts +++ b/packages/adapters/claude-local/src/server/setup-token-runner.test.ts @@ -11,7 +11,7 @@ import { // A well-formed authorization URL. The query keys match the contract exactly. // The values are synthetic; no real value is present. const VALID_URL = - "https://claude.com/cai/oauth/authorize?client_id=cid&code=abc&code_challenge=chal&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fexample.test%2Fcb&response_type=code&scope=user&state=st"; + "https://claude.com/cai/oauth/authorize?client_id=cid&code=abcdefgh&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fplatform.claude.com%2Foauth%2Fcode%2Fcallback&response_type=code&scope=user&state=0123456789abcdef"; // The rendered login output. The preamble line, the URL line, and the dedicated // prompt line match the Phase 2 parser contract. @@ -97,11 +97,14 @@ async function flush(): Promise { describe("runSetupTokenLogin", () => { it("sends one login URL through onPrompt", async () => { - const fake = createFakeDriver({ chunks: [PROMPT_OUTPUT], exitCode: 0 }); + // A success stream and a resolving sink, so the run reports success after the + // runner delivers the credential. + const fake = createFakeDriver({ chunks: [PROMPT_OUTPUT, SUCCESS_OUTPUT], exitCode: 0 }); const onPrompt = vi.fn(); const result = await runSetupTokenLogin(fake.driver, { onPrompt, provideCode: async () => BROWSER_CODE, + onCredential: async () => {}, timeoutMs: 1000, }); expect(result.outcome).toBe("success"); @@ -111,12 +114,15 @@ describe("runSetupTokenLogin", () => { }); it("accepts one browser code and sends it to the prompt", async () => { - const fake = createFakeDriver({ chunks: [PROMPT_OUTPUT] }); + // The stream carries the prompt and the success record. The runner submits the + // code, then it delivers the credential and reports success after the sink. + const fake = createFakeDriver({ chunks: [PROMPT_OUTPUT, SUCCESS_OUTPUT] }); const onPrompt = vi.fn(); const provideCode = vi.fn(async () => BROWSER_CODE); const promise = runSetupTokenLogin(fake.driver, { onPrompt, provideCode, + onCredential: async () => {}, timeoutMs: 1000, codeSubmitSettleMs: 0, }); @@ -247,27 +253,34 @@ describe("runSetupTokenLogin", () => { expect(onPrompt).toHaveBeenCalledWith({ url: VALID_URL, prompt: SETUP_TOKEN_PROMPT }); }); - it("delivers no token when the success stream has no token block", async () => { + it("fails when a clean exit has no token block, even with a sink present", async () => { // The stream holds only the prompt, so the token parser binds nothing. The - // runner delivers no credential and reports credentialDelivered false. + // runner treats a clean exit with no bound token as a failure. It never calls + // the sink and never reports success. const fake = createFakeDriver({ chunks: [PROMPT_OUTPUT], exitCode: 0 }); - const onCredential = vi.fn(); + const onCredential = vi.fn(async () => {}); const result = await runSetupTokenLogin(fake.driver, { onPrompt: () => {}, provideCode: async () => BROWSER_CODE, onCredential, timeoutMs: 1000, }); - expect(result.outcome).toBe("success"); + expect(result.outcome).toBe("failure"); expect(onCredential).not.toHaveBeenCalled(); expect(result.credentialDelivered).toBe(false); expect(result).not.toHaveProperty("token"); expect(result).not.toHaveProperty("credential"); }); - it("delivers the de-wrapped token once through onCredential on a success stream", async () => { + it("delivers the de-wrapped token once through onCredential and zeros it after", async () => { const fake = createFakeDriver({ chunks: [PROMPT_OUTPUT, SUCCESS_OUTPUT], exitCode: 0 }); - const onCredential = vi.fn(); + // The sink copies the token value, because the runner zeros the buffer after + // the delivery. The test reads the copy for the value and the original for the + // zeroing proof. + let received: string | null = null; + const onCredential = vi.fn(async (bytes: Buffer) => { + received = bytes.toString("utf8"); + }); const result = await runSetupTokenLogin(fake.driver, { onPrompt: () => {}, provideCode: async () => BROWSER_CODE, @@ -277,9 +290,11 @@ describe("runSetupTokenLogin", () => { expect(result.outcome).toBe("success"); expect(result.credentialDelivered).toBe(true); expect(onCredential).toHaveBeenCalledTimes(1); + expect(received).toBe(FULL_TOKEN); + // The runner zeros the token bytes after a successful delivery. const bytes = onCredential.mock.calls[0][0] as Buffer; expect(Buffer.isBuffer(bytes)).toBe(true); - expect(bytes.toString("utf8")).toBe(FULL_TOKEN); + expect(bytes.every((byte) => byte === 0)).toBe(true); }); it("keeps the token out of every log, result, and error field", async () => { @@ -288,7 +303,7 @@ describe("runSetupTokenLogin", () => { const result = await runSetupTokenLogin(fake.driver, { onPrompt: () => {}, provideCode: async () => BROWSER_CODE, - onCredential: () => {}, + onCredential: async () => {}, timeoutMs: 1000, log: (line) => { logs.push(line); @@ -300,19 +315,60 @@ describe("runSetupTokenLogin", () => { expect(result.credentialDelivered).toBe(true); }); - it("never scans for the token when no onCredential sink is present", async () => { - // With no sink the runner never holds the post-prompt stream and delivers - // nothing, even when the success block is present. + it("fails closed when no onCredential sink is present, even on a success stream", async () => { + // The sink is mandatory for a successful run. With no sink the runner never + // scans for the token and treats the clean exit as a failure, even when the + // success block is present. const fake = createFakeDriver({ chunks: [PROMPT_OUTPUT, SUCCESS_OUTPUT], exitCode: 0 }); const result = await runSetupTokenLogin(fake.driver, { onPrompt: () => {}, provideCode: async () => BROWSER_CODE, timeoutMs: 1000, }); - expect(result.outcome).toBe("success"); + expect(result.outcome).toBe("failure"); expect(result.credentialDelivered).toBe(false); }); + it("fails and zeros the token when the sink throws synchronously", async () => { + const fake = createFakeDriver({ chunks: [PROMPT_OUTPUT, SUCCESS_OUTPUT], exitCode: 0 }); + let seen: Buffer | null = null; + const result = await runSetupTokenLogin(fake.driver, { + onPrompt: () => {}, + provideCode: async () => BROWSER_CODE, + // A synchronous throw. The runner catches it, fails closed, and never + // reports success. + onCredential: (bytes) => { + seen = bytes; + throw new Error("synchronous sink failure"); + }, + timeoutMs: 1000, + }); + expect(result.outcome).toBe("failure"); + expect(result.credentialDelivered).toBe(false); + expect(seen).not.toBeNull(); + expect((seen as unknown as Buffer).every((byte) => byte === 0)).toBe(true); + }); + + it("fails and zeros the token when the sink rejects asynchronously", async () => { + const fake = createFakeDriver({ chunks: [PROMPT_OUTPUT, SUCCESS_OUTPUT], exitCode: 0 }); + let seen: Buffer | null = null; + const result = await runSetupTokenLogin(fake.driver, { + onPrompt: () => {}, + provideCode: async () => BROWSER_CODE, + // An asynchronous rejection. The runner does not swallow it: it maps the + // rejection to a failure and never reports success. + onCredential: async (bytes) => { + seen = bytes; + throw new Error("asynchronous sink failure"); + }, + timeoutMs: 1000, + }); + expect(result.outcome).toBe("failure"); + expect(result.credentialDelivered).toBe(false); + expect(seen).not.toBeNull(); + expect((seen as unknown as Buffer).every((byte) => byte === 0)).toBe(true); + }); + it("exposes the fixed setup-token command", () => { expect(CLAUDE_SETUP_TOKEN_COMMAND).toBe("claude setup-token"); }); diff --git a/packages/adapters/claude-local/src/server/setup-token-runner.ts b/packages/adapters/claude-local/src/server/setup-token-runner.ts index 1d06a46e98..17521610b0 100644 --- a/packages/adapters/claude-local/src/server/setup-token-runner.ts +++ b/packages/adapters/claude-local/src/server/setup-token-runner.ts @@ -19,14 +19,17 @@ import { // time through the in-memory `onPrompt` callback. It reads the browser code one // time through the in-memory `provideCode` callback and writes the code only to // the child, only after it matches the prompt. It delivers the token one time -// through the in-memory `onCredential` callback. The runner keeps the URL, the -// code, and any token byte out of every log line and every thrown error. +// through the in-memory `onCredential` sink. The runner keeps the URL, the code, +// and any token byte out of every log line and every thrown error. // -// Token delivery binds to the success record. The runner scans the post-prompt -// stream with {@link parseSetupTokenCredential}, which returns the token only -// from the exact success record. The {@link SETUP_TOKEN_CREDENTIAL_RELEASE_GATE} -// stays open only while a caller provides an `onCredential` sink; without a sink -// the runner never scans for the token. +// Fail-closed credential delivery. The `onCredential` sink is mandatory and +// awaited. The runner binds the token from the exact success record with +// {@link parseSetupTokenCredential}, then it awaits the sink one time. It sets +// `credentialDelivered` true only after the sink resolves. A missing sink, a +// success stream with no token, and a sink rejection each end the run as a +// failure; the runner never reports success without a resolved sink. The runner +// zeros the token bytes in a `finally` block on every path, so the secret never +// outlives the delivery. /** The fixed Claude setup-token command. The login flow needs a PTY. */ export const CLAUDE_SETUP_TOKEN_COMMAND = "claude setup-token"; @@ -61,14 +64,6 @@ export const CODE_SUBMIT_SETTLE_MS = 150; */ export const CLAUDE_SETUP_TOKEN_MAX_BUFFER_CHARS = 64 * 1024; -/** - * The release gate for the credential seam. The gate is open, so the runner - * delivers the token that {@link parseSetupTokenCredential} binds from the - * success record. The runner still scans for the token only when a caller - * provides an `onCredential` sink. - */ -export const SETUP_TOKEN_CREDENTIAL_RELEASE_GATE: boolean = true; - /** * The child side of the setup-token run. The runner never spawns the PTY * directly; a caller injects a concrete driver. A production driver binds these @@ -108,11 +103,12 @@ export type SetupTokenCodeProvider = (signal: AbortSignal) => Promise; /** * Receives the credential bytes one time in memory on success. The runner binds - * the token from the success record, then it invokes this sink one time with the - * token bytes. The runner never logs the bytes and never stores them on the - * result. + * the token from the success record, then it awaits this sink one time with the + * token bytes. The sink is mandatory and it returns a promise; the runner awaits + * that promise before it reports success. A sink rejection ends the run as a + * failure. The runner never logs the bytes and never stores them on the result. */ -export type SetupTokenCredentialSink = (authBytes: Buffer) => void | Promise; +export type SetupTokenCredentialSink = (authBytes: Buffer) => Promise; export type SetupTokenOutcome = "success" | "failure" | "timeout" | "cancelled"; @@ -133,7 +129,12 @@ export interface RunSetupTokenLoginOptions { onPrompt: SetupTokenPromptSink; /** Returns the one browser code. The runner writes it to the matched prompt. */ provideCode: SetupTokenCodeProvider; - /** The credential sink. The runner invokes it one time with the token bytes. */ + /** + * The credential sink. The runner awaits it one time with the token bytes and + * reports success only after it resolves. The sink is mandatory for a + * successful run: a missing sink ends the run as a failure. The option stays + * optional so a caller can drive a fail-closed path with no sink. + */ onCredential?: SetupTokenCredentialSink; /** * The settle delay in milliseconds between the code write and the terminator @@ -256,13 +257,16 @@ export async function runSetupTokenLogin( let promptSurfaced = false; let codeSubmitted = false; let submitStarted = false; + // The runner sets this true one time, only after the mandatory sink resolves. let credentialDelivered = false; + // The bound token bytes. The runner binds them from the success record during + // the stream, holds them in memory only, awaits the sink one time after a + // clean exit, and zeros them in a `finally` block on every path. A null value + // means the runner never bound a token. + let tokenBytes: Buffer | null = null; // The code-input routine runs beside the race. It is self-guarding and never // rejects, so an unresolved provider cannot become an unhandled rejection. let submitPromise: Promise = Promise.resolve(); - // The credential-delivery routine runs beside the race. It is self-guarding - // and never rejects, so an async sink cannot become an unhandled rejection. - let credentialPromise: Promise = Promise.resolve(); // The in-memory prompt buffer. The runner drops it as soon as it finds the // prompt, so the secret-bearing stream never lives longer than one parse. The @@ -306,24 +310,39 @@ export async function runSetupTokenLogin( } }; - // Binds the token from the token buffer and delivers it one time. The runner - // scans only when a caller provides an `onCredential` sink and the gate is - // open. It drops the token buffer as soon as the parser binds the token. It - // never logs the token and never puts it into a thrown error. + // Binds the token from the token buffer one time and holds the bytes in + // memory. The runner scans only when a caller provides an `onCredential` sink, + // so it never holds the secret-bearing stream without a need. It drops the + // token buffer as soon as the parser binds the token. The runner delivers the + // bytes later, after a clean exit, through {@link deliverCredential}. const captureCredential = (): void => { - if (!SETUP_TOKEN_CREDENTIAL_RELEASE_GATE || !onCredential || credentialDelivered) return; + if (!onCredential || tokenBytes) return; const token = parseSetupTokenCredential(tokenBuffer); if (!token) return; - credentialDelivered = true; + tokenBytes = Buffer.from(token, "utf8"); tokenBuffer = ""; - const authBytes = Buffer.from(token, "utf8"); + }; + + // Awaits the mandatory sink one time with the bound token bytes and reports + // whether the credential landed. It returns false when the sink is missing, + // when the runner bound no token, or when the sink rejects. It does not swallow + // a sink rejection: it maps the rejection to a false result, so the caller ends + // the run as a failure. It zeros the token bytes in a `finally` block on every + // path, so the secret never outlives the delivery. + const deliverCredential = async (): Promise => { + const bytes = tokenBytes; + tokenBytes = null; + if (!bytes) return false; try { - credentialPromise = Promise.resolve(onCredential(authBytes)).catch(() => { - log("[paperclip] Setup-token login: the credential delivery step errored."); - }); + if (!onCredential) return false; + await onCredential(bytes); log("[paperclip] Setup-token login: delivered the credential to the sink."); + return true; } catch { log("[paperclip] Setup-token login: the credential delivery step errored."); + return false; + } finally { + bytes.fill(0); } }; @@ -352,10 +371,10 @@ export async function runSetupTokenLogin( // The prompt already surfaced. Scan the post-prompt stream for the success // token. The runner scans only when a caller provides an `onCredential` // sink, so it never holds the secret-bearing stream without a need. - if (!SETUP_TOKEN_CREDENTIAL_RELEASE_GATE || !onCredential || credentialDelivered) return; + if (!onCredential || tokenBytes) return; tokenBuffer += chunk; captureCredential(); - if (!credentialDelivered && tokenBuffer.length > CLAUDE_SETUP_TOKEN_MAX_BUFFER_CHARS) { + if (!tokenBytes && tokenBuffer.length > CLAUDE_SETUP_TOKEN_MAX_BUFFER_CHARS) { tokenBuffer = tokenBuffer.slice(tokenBuffer.length - CLAUDE_SETUP_TOKEN_MAX_BUFFER_CHARS); } }; @@ -396,10 +415,16 @@ export async function runSetupTokenLogin( return result("failure", exitCode); } - // The runner captured the token from the stream during `onData` and - // delivered it through `onCredential`. Await a pending async sink, so the - // credential fully lands before the runner reports success. - await credentialPromise; + // The command exited cleanly. Deliver the bound token through the mandatory + // sink and await it. The runner fails closed: a missing sink, a clean exit + // with no bound token, and a sink rejection each end the run as a failure. It + // reports success only after the sink resolves. + const delivered = await deliverCredential(); + if (!delivered) { + log("[paperclip] Setup-token login: the credential did not land; treating the run as a failure."); + return result("failure", exitCode); + } + credentialDelivered = true; log("[paperclip] Setup-token login command ended successfully."); return result("success", exitCode); @@ -409,6 +434,14 @@ export async function runSetupTokenLogin( throw new Error("setup-token login failed: the sandbox login command errored."); } finally { if (signal) signal.removeEventListener("abort", onExternalAbort); + // Zero any bound token bytes that the delivery path did not consume, so a + // timeout, a cancellation, or an error never leaves the secret in memory. The + // cast restores the type that the closure assignment widens away. + const leftover = tokenBytes as Buffer | null; + if (leftover) { + leftover.fill(0); + tokenBytes = null; + } // Stop a pending code-input routine, then stop the child and dispose. controller.abort(); await stopAndDispose(driver, log); diff --git a/packages/adapters/claude-local/src/server/test.probe.test.ts b/packages/adapters/claude-local/src/server/test.probe.test.ts index 58088fa47d..272ce35d3c 100644 --- a/packages/adapters/claude-local/src/server/test.probe.test.ts +++ b/packages/adapters/claude-local/src/server/test.probe.test.ts @@ -75,15 +75,19 @@ afterEach(() => { }); describe("claude sandbox hello probe diagnostics", () => { - it("surfaces the final result error instead of the system/init line on failure", async () => { + it("keeps the raw failure result out of every check and routes it to the log", async () => { + // The non-zero result event carries a marker. The check must not repeat the + // marker, and the redacted diagnostic must reach the server log. + const marker = "NONPATTERNMARKERfailure"; probeResult.value = { exitCode: 1, stdout: [ initLine, - '{"type":"result","subtype":"error_during_execution","is_error":true,"result":"API Error: 404 model not found: claude-opus-4-8","session_id":"abc"}', + `{"type":"result","subtype":"error_during_execution","is_error":true,"result":"API Error: 404 model not found ${marker}","session_id":"abc"}`, ].join("\n"), stderr: "", }; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const result = await testEnvironment({ companyId: "company-1", @@ -96,9 +100,228 @@ describe("claude sandbox hello probe diagnostics", () => { expect(result.status).toBe("fail"); const failed = result.checks.find((check) => check.code === "claude_hello_probe_failed"); expect(failed).toBeTruthy(); - expect(failed?.detail).toContain("404 model not found: claude-opus-4-8"); - // The unhelpful init line must not be what we show the operator. - expect(failed?.detail).not.toContain('"subtype":"init"'); + // The public check carries only a fixed message and hint, no raw detail. + expect(failed?.detail).toBeUndefined(); + const checkText = JSON.stringify(result.checks); + expect(checkText).not.toContain(marker); + // The unhelpful init line must never reach a check either. + expect(checkText).not.toContain('"subtype":"init"'); + // The raw diagnostic still reaches the server log. + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).toContain(marker); + warnSpy.mockRestore(); + }); + + it("keeps a stdout-fallback failure line out of every check", async () => { + // The CLI dies before a result event, so the last non-init stdout line is + // the diagnostic. The check must not repeat its marker. + const marker = "NONPATTERNMARKERstdout"; + probeResult.value = { + exitCode: 1, + stdout: [initLine, `fatal: claude crashed ${marker}`].join("\n"), + stderr: "", + }; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { engine: "cli", command: "claude" }, + executionTarget: sandboxTarget, + environmentName: "Daytona", + }); + + const failed = result.checks.find((check) => check.code === "claude_hello_probe_failed"); + expect(failed).toBeTruthy(); + expect(failed?.detail).toBeUndefined(); + const checkText = JSON.stringify(result.checks); + expect(checkText).not.toContain(marker); + expect(checkText).not.toContain('"subtype":"init"'); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).toContain(marker); + warnSpy.mockRestore(); + }); + + it("never copies a credential-bearing stderr failure line into a check", async () => { + // A verbose CLI can print a credential to stderr on failure. The check must + // not repeat it, and the server log must redact it. + const secret = "sk-ant-STDERRLEAK0123456789abcdef"; + probeResult.value = { + exitCode: 1, + stdout: initLine, + stderr: `fatal: request failed with token ${secret}`, + }; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { engine: "cli", command: "claude" }, + executionTarget: sandboxTarget, + environmentName: "Daytona", + }); + + const checkText = JSON.stringify(result.checks); + expect(checkText).not.toContain(secret); + expect(checkText).not.toContain("STDERRLEAK"); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).not.toContain(secret); + expect(loggedText).toContain("***REDACTED***"); + warnSpy.mockRestore(); + }); + + it("keeps an auth-required probe marker out of every check", async () => { + // The auth-required stdout and stderr carry a marker. The login-required + // checks must not repeat it, and the login gate code must stay stable. + const marker = "NONPATTERNMARKERauthcli"; + probeResult.value = { + exitCode: 1, + stdout: [ + initLine, + `{"type":"result","subtype":"error_during_execution","is_error":true,"result":"Please run \`claude login\` ${marker}","session_id":"abc"}`, + ].join("\n"), + stderr: `Please run claude login ${marker}`, + }; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { engine: "cli", command: "claude" }, + executionTarget: sandboxTarget, + environmentName: "Daytona", + }); + + expect(result.checks.some((check) => check.code === "claude_hello_probe_auth_required")).toBe(true); + // The login gate code stays stable so the user interface can offer login. + expect(result.checks.some((check) => check.code === "adapter_auth_missing")).toBe(true); + const checkText = JSON.stringify(result.checks); + expect(checkText).not.toContain(marker); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).toContain(marker); + warnSpy.mockRestore(); + }); + + it("classifies an invalid or expired token as adapter_auth_missing without leaking the token", async () => { + // Grounded on the real Claude CLI output for CLAUDE_CODE_OAUTH_TOKEN=invalid. + // The probe exits non-zero and the result event reports a 401 authentication + // failure with an "Invalid bearer token" message. A synthetic bearer marker + // rides along on a retry line, so the test proves the raw text never reaches + // a check. + const marker = "SUPERSECRETbearerMARKERcli"; + probeResult.value = { + exitCode: 1, + stdout: [ + initLine, + `{"type":"system","subtype":"api_retry","attempt":1,"error_status":401,"error":"authentication_failed: bearer ${marker} is invalid","session_id":"abc"}`, + '{"type":"result","subtype":"success","is_error":true,"api_error_status":401,"error":"authentication_failed","result":"Failed to authenticate. API Error: 401 Invalid bearer token","session_id":"abc"}', + ].join("\n"), + stderr: "", + }; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { engine: "cli", command: "claude" }, + executionTarget: sandboxTarget, + environmentName: "Daytona", + }); + + // An auth failure returns the canonical login gate code, so the user + // interface can offer login. + expect(result.checks.some((check) => check.code === "claude_hello_probe_auth_required")).toBe(true); + expect(result.checks.some((check) => check.code === "adapter_auth_missing")).toBe(true); + // The raw probe text, including the bearer marker, never reaches a check. + expect(JSON.stringify(result.checks)).not.toContain(marker); + warnSpy.mockRestore(); + }); + + it("does not flag a healthy probe whose assistant text repeats a token phrase", async () => { + // A healthy run prints an auth phrase in its answer text. The parsed result + // is a success, so the probe stays healthy and offers no login gate. + probeResult.value = { + exitCode: 0, + stdout: [ + initLine, + '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hello — authentication_failed means an invalid bearer token"}]},"session_id":"abc"}', + '{"type":"result","subtype":"success","is_error":false,"result":"hello","session_id":"abc"}', + ].join("\n"), + stderr: "", + }; + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { engine: "cli", command: "claude" }, + executionTarget: sandboxTarget, + environmentName: "Daytona", + }); + + expect(result.checks.some((check) => check.code === "adapter_auth_missing")).toBe(false); + expect(result.checks.some((check) => check.code === "claude_hello_probe_auth_required")).toBe(false); + expect(result.checks.some((check) => check.code === "claude_hello_probe_passed")).toBe(true); + }); + + it("keeps a transient failure with an assistant token phrase off the login gate", async () => { + // The probe fails on a 529 overload. The auth phrase appears only in the raw + // stdout assistant event, not the parsed result, so the run stays transient + // and never surfaces the login gate. + probeResult.value = { + exitCode: 1, + stdout: [ + initLine, + '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"authentication_failed: the bearer token is invalid"}]},"session_id":"abc"}', + '{"type":"result","subtype":"error_during_execution","is_error":true,"result":"API Error: 529 overloaded_error","session_id":"abc"}', + ].join("\n"), + stderr: "", + }; + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { engine: "cli", command: "claude" }, + executionTarget: sandboxTarget, + environmentName: "Daytona", + }); + + expect(result.checks.some((check) => check.code === "adapter_auth_missing")).toBe(false); + expect(result.checks.some((check) => check.code === "claude_hello_probe_auth_required")).toBe(false); + expect(result.checks.some((check) => check.code === "claude_hello_probe_transient_upstream")).toBe(true); + }); + + it("keeps an unexpected successful summary out of every check", async () => { + // The probe exits 0 but does not return `hello`. The unexpected summary is + // untrusted output. The check must not repeat its marker. + const marker = "NONPATTERNMARKERunexpected"; + probeResult.value = { + exitCode: 0, + stdout: [ + initLine, + `{"type":"result","subtype":"success","is_error":false,"result":"Here is something else ${marker}","session_id":"abc"}`, + ].join("\n"), + stderr: "", + }; + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { engine: "cli", command: "claude" }, + executionTarget: sandboxTarget, + environmentName: "Daytona", + }); + + const unexpected = result.checks.find( + (check) => check.code === "claude_hello_probe_unexpected_output", + ); + expect(unexpected).toBeTruthy(); + expect(unexpected?.detail).toBeUndefined(); + const checkText = JSON.stringify(result.checks); + expect(checkText).not.toContain(marker); + const loggedText = JSON.stringify(warnSpy.mock.calls); + expect(loggedText).toContain(marker); + warnSpy.mockRestore(); }); it("classifies subscription usage-limit failures as a usage-limited warning, not a hard fail", async () => { @@ -147,26 +370,7 @@ describe("claude sandbox hello probe diagnostics", () => { expect(result.checks.some((check) => check.code === "claude_hello_probe_failed")).toBe(false); }); - it("falls back to the last stdout line when no result event is emitted", async () => { - probeResult.value = { - exitCode: 1, - stdout: [initLine, "fatal: claude crashed unexpectedly"].join("\n"), - stderr: "", - }; - - const result = await testEnvironment({ - companyId: "company-1", - adapterType: "claude_local", - config: { engine: "cli", command: "claude" }, - executionTarget: sandboxTarget, - environmentName: "Daytona", - }); - - const failed = result.checks.find((check) => check.code === "claude_hello_probe_failed"); - expect(failed?.detail).toContain("claude crashed unexpectedly"); - }); - - it("does not show the system/init event when it is the only stdout line", async () => { + it("keeps the failed check free of a detail when only the system/init line is present", async () => { probeResult.value = { exitCode: 1, stdout: initLine, @@ -183,6 +387,7 @@ describe("claude sandbox hello probe diagnostics", () => { const failed = result.checks.find((check) => check.code === "claude_hello_probe_failed"); expect(failed?.detail).toBeUndefined(); + expect(JSON.stringify(result.checks)).not.toContain('"subtype":"init"'); }); }); diff --git a/packages/adapters/claude-local/src/server/test.remote.test.ts b/packages/adapters/claude-local/src/server/test.remote.test.ts new file mode 100644 index 0000000000..3c7f5d5fd1 --- /dev/null +++ b/packages/adapters/claude-local/src/server/test.remote.test.ts @@ -0,0 +1,139 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target"; + +const { + ensureAdapterExecutionTargetDirectory, + ensureAdapterExecutionTargetCommandResolvable, + maybeRunSandboxInstallCommand, + runAdapterExecutionTargetProcess, + describeAdapterExecutionTarget, + resolveAdapterExecutionTargetCwd, + probeResult, +} = vi.hoisted(() => { + const probeResult: { value: { exitCode: number; stdout: string; stderr: string } } = { + value: { exitCode: 1, stdout: "", stderr: "" }, + }; + return { + probeResult, + ensureAdapterExecutionTargetDirectory: vi.fn(async () => {}), + ensureAdapterExecutionTargetCommandResolvable: vi.fn(async () => {}), + maybeRunSandboxInstallCommand: vi.fn(async () => null), + runAdapterExecutionTargetProcess: vi.fn(async () => ({ + exitCode: probeResult.value.exitCode, + signal: null, + timedOut: false, + stdout: probeResult.value.stdout, + stderr: probeResult.value.stderr, + pid: 123, + startedAt: new Date().toISOString(), + })), + describeAdapterExecutionTarget: vi.fn(() => "Daytona"), + resolveAdapterExecutionTargetCwd: vi.fn(() => "/home/daytona/paperclip-workspace"), + }; +}); + +vi.mock("@paperclipai/adapter-utils/execution-target", async () => { + const actual = await vi.importActual( + "@paperclipai/adapter-utils/execution-target", + ); + return { + ...actual, + ensureAdapterExecutionTargetDirectory, + ensureAdapterExecutionTargetCommandResolvable, + maybeRunSandboxInstallCommand, + runAdapterExecutionTargetProcess, + describeAdapterExecutionTarget, + resolveAdapterExecutionTargetCwd, + }; +}); + +import { testEnvironment } from "./test.js"; +import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js"; + +const sandboxTarget: AdapterExecutionTarget = { + kind: "remote", + transport: "sandbox", + providerKey: "daytona", + remoteCwd: "/home/daytona/paperclip-workspace", + runner: { + execute: async () => ({ + exitCode: 0, + signal: null, + timedOut: false, + stdout: "", + stderr: "", + pid: null, + startedAt: new Date().toISOString(), + }), + }, +}; + +const sshTarget: AdapterExecutionTarget = { + kind: "remote", + transport: "ssh", + remoteCwd: "/home/agent/paperclip-workspace", + spec: { + host: "ssh.example.test", + port: 22, + username: "agent", + remoteCwd: "/home/agent/paperclip-workspace", + remoteWorkspacePath: "/home/agent/paperclip-workspace", + privateKey: null, + knownHosts: null, + strictHostKeyChecking: true, + }, +}; + +const initLine = + '{"type":"system","subtype":"init","cwd":"/home/daytona/paperclip-workspace","session_id":"abc","tools":["Bash","Read"]}'; + +const loginRequiredStdout = [ + initLine, + '{"type":"result","subtype":"error_during_execution","is_error":true,"result":"Please run `claude login` to authenticate.","session_id":"abc"}', +].join("\n"); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe("claude sandbox auth-missing check", () => { + it("emits the canonical adapter_auth_missing check when a sandbox hello probe reports missing auth", async () => { + // The sandbox has no ready login, so the hello probe returns a + // login-required result. The Test must emit the neutral canonical check + // code. The user interface reads this code to decide login eligibility; it + // does not parse the message text or the top-level status. + probeResult.value = { exitCode: 1, stdout: loginRequiredStdout, stderr: "" }; + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { engine: "cli", command: "claude" }, + executionTarget: sandboxTarget, + environmentName: "Daytona", + }); + + // A missing-auth probe is a warning, not a failure, so the environment stays + // testable and the user interface can offer login. + expect(result.status).toBe("warn"); + expect(result.checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(true); + // The descriptive probe check stays, so existing diagnostics keep working. + expect(result.checks.some((check) => check.code === "claude_hello_probe_auth_required")).toBe(true); + }); + + it("does not emit adapter_auth_missing when a non-sandbox remote probe reports missing auth", async () => { + // Only a sandbox target can start login. A non-sandbox remote target keeps + // the descriptive probe check but does not carry the neutral login code. + probeResult.value = { exitCode: 1, stdout: loginRequiredStdout, stderr: "" }; + + const result = await testEnvironment({ + companyId: "company-1", + adapterType: "claude_local", + config: { engine: "cli", command: "claude" }, + executionTarget: sshTarget, + environmentName: "Remote host", + }); + + expect(result.checks.some((check) => check.code === "claude_hello_probe_auth_required")).toBe(true); + expect(result.checks.some((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(false); + }); +}); diff --git a/packages/adapters/claude-local/src/server/test.ts b/packages/adapters/claude-local/src/server/test.ts index 7520137024..ef112d03ad 100644 --- a/packages/adapters/claude-local/src/server/test.ts +++ b/packages/adapters/claude-local/src/server/test.ts @@ -1,6 +1,3 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; import type { AdapterEnvironmentCheck, AdapterEnvironmentTestContext, @@ -18,12 +15,9 @@ import { import { ensureAdapterExecutionTargetCommandResolvable, ensureAdapterExecutionTargetDirectory, - maybeRunSandboxInstallCommand, - prepareAdapterExecutionTargetRuntime, runAdapterExecutionTargetProcess, describeAdapterExecutionTarget, resolveAdapterExecutionTargetCwd, - adapterExecutionTargetUsesManagedHome, } from "@paperclipai/adapter-utils/execution-target"; import { describeClaudeFailure, @@ -35,9 +29,14 @@ import { import { claudeCommandLooksLike, claudeCommandSupportsEffortFlag } from "./cli-capabilities.js"; import { isBedrockModelId } from "./models.js"; import { buildClaudeProbePermissionArgs } from "./permissions.js"; -import { materializeRemoteClaudeConfig, prepareClaudeConfigSeed } from "./claude-config.js"; +import { prepareSandboxClaudeProbeRuntime } from "./claude-config.js"; import { SANDBOX_INSTALL_COMMAND } from "../index.js"; import { resolveClaudeExecutionEngineForRun, testClaudeAcpEnvironment } from "./acp.js"; +import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./auth-check.js"; +import { + buildClaudeLoginRequiredHint, + logRedactedSandboxProbeDiagnostic, +} from "./probe-diagnostics.js"; function summarizeStatus(checks: AdapterEnvironmentCheck[]): AdapterEnvironmentTestResult["status"] { if (checks.some((check) => check.level === "error")) return "fail"; @@ -74,11 +73,6 @@ function lastNonInitStdoutLine(text: string): string { return ""; } -function truncateDetail(value: string, max = 240): string { - const clean = value.replace(/\s+/g, " ").trim(); - return clean.length > max ? `${clean.slice(0, max - 1)}…` : clean; -} - function summarizeProbeDetail(stdout: string, stderr: string): string | null { const raw = firstNonEmptyLine(stderr) || lastNonInitStdoutLine(stdout); if (!raw) return null; @@ -152,77 +146,20 @@ export async function testEnvironment( for (const [key, value] of Object.entries(envConfig)) { if (typeof value === "string") env[key] = value; } - const installCheck = await maybeRunSandboxInstallCommand({ - runId, - target, - adapterKey: "claude", - installCommand: SANDBOX_INSTALL_COMMAND, - detectCommand: command, - env, - }); - if (installCheck) checks.push(installCheck); - const hasExplicitClaudeConfigDir = isNonEmpty(env.CLAUDE_CONFIG_DIR); - if (targetIsRemote && adapterExecutionTargetUsesManagedHome(target) && !hasExplicitClaudeConfigDir) { - let tempWorkspaceDir: string | null = null; - let preparedRuntime: Awaited> | null = null; - try { - const seedDir = await prepareClaudeConfigSeed(process.env, async () => {}, ctx.companyId); - const managedRemoteCwd = target?.kind === "remote" ? target.remoteCwd : cwd; - tempWorkspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-claude-envtest-workspace-")); - preparedRuntime = await prepareAdapterExecutionTargetRuntime({ - runId, - target, - adapterKey: "claude", - workspaceLocalDir: tempWorkspaceDir, - workspaceRemoteDir: managedRemoteCwd, - timeoutSec: Math.max(1, asNumber(config.helloProbeTimeoutSec, targetIsSandbox ? 90 : 45)), - assets: [ - { - key: "config-seed", - localDir: seedDir, - followSymlinks: true, - }, - ], - }); - const runtimeRootDir = - preparedRuntime.runtimeRootDir ?? path.posix.join(managedRemoteCwd, ".paperclip-runtime", "claude"); - const remoteClaudeConfigSeedDir = - preparedRuntime.assetDirs["config-seed"] ?? path.posix.join(runtimeRootDir, "config-seed"); - const remoteClaudeConfigDir = path.posix.join(runtimeRootDir, "config"); - env.CLAUDE_CONFIG_DIR = remoteClaudeConfigDir; - await materializeRemoteClaudeConfig({ - runId, - target, - remoteClaudeConfigDir, - remoteClaudeConfigSeedDir, - options: { - cwd, - env, - timeoutSec: Math.max(15, asNumber(config.helloProbeTimeoutSec, targetIsSandbox ? 90 : 45)), - graceSec: 5, - onLog: async () => {}, - }, - }); - checks.push({ - code: "claude_managed_config_dir", - level: "info", - message: "Sandbox probe is using Paperclip-managed Claude config materialization.", - detail: remoteClaudeConfigDir, - }); - } catch (err) { - checks.push({ - code: "claude_managed_config_dir_failed", - level: "error", - message: "Could not materialize Paperclip-managed Claude config for the sandbox probe.", - detail: err instanceof Error ? err.message : String(err), - }); - } finally { - await preparedRuntime?.restoreWorkspace().catch(() => undefined); - if (tempWorkspaceDir) { - await fs.rm(tempWorkspaceDir, { recursive: true, force: true }).catch(() => undefined); - } - } - } + checks.push( + ...(await prepareSandboxClaudeProbeRuntime({ + runId, + target, + cwd, + companyId: ctx.companyId, + env, + installCommand: SANDBOX_INSTALL_COMMAND, + detectCommand: command, + targetIsRemote, + targetIsSandbox, + helloProbeTimeoutSec: asNumber(config.helloProbeTimeoutSec, targetIsSandbox ? 90 : 45), + })), + ); const runtimeEnv = ensurePathInEnv({ ...process.env, ...env }); try { await ensureAdapterExecutionTargetCommandResolvable(command, target, cwd, runtimeEnv); @@ -397,7 +334,6 @@ export async function testEnvironment( stdout: probe.stdout, stderr: probe.stderr, }); - const detail = summarizeProbeDetail(probe.stdout, probe.stderr); if (probe.timedOut) { checks.push({ @@ -407,25 +343,46 @@ export async function testEnvironment( hint: "Retry the probe. If this persists, verify Claude can run `Respond with hello` from this directory manually.", }); } else if (loginMeta.requiresLogin) { + // The raw probe output is untrusted. Route it to the log-only boundary + // and return only a fixed public message and a safe hint. + logRedactedSandboxProbeDiagnostic( + "Claude CLI hello probe reported login required", + summarizeProbeDetail(probe.stdout, probe.stderr), + ); checks.push({ code: "claude_hello_probe_auth_required", level: "warn", message: "Claude CLI is installed, but login is required.", - ...(detail ? { detail } : {}), - hint: loginMeta.loginUrl - ? `Run \`claude login\` and complete sign-in at ${loginMeta.loginUrl}, then retry.` - : "Run `claude login` in this environment, then retry the probe.", + hint: buildClaudeLoginRequiredHint(loginMeta.loginUrl), }); + if (targetIsSandbox) { + // Emit the neutral canonical check so the user interface can decide + // login eligibility from a stable code. The user interface does not + // read the message text or the top-level status. + checks.push({ + code: ADAPTER_AUTH_MISSING_CHECK_CODE, + level: "warn", + message: "The sandbox has no ready authentication for this adapter.", + hint: "Provide credentials for this adapter, or start login in the sandbox.", + }); + } } else if ((probe.exitCode ?? 1) === 0) { const summary = parsedStream.summary.trim(); const hasHello = /\bhello\b/i.test(summary); + if (!hasHello) { + // The unexpected summary is untrusted probe output. Route it to the + // log-only boundary and keep the check text fixed. + logRedactedSandboxProbeDiagnostic( + "Claude CLI hello probe returned unexpected output", + summary, + ); + } checks.push({ code: hasHello ? "claude_hello_probe_passed" : "claude_hello_probe_unexpected_output", level: hasHello ? "info" : "warn", message: hasHello ? "Claude hello probe succeeded." : "Claude probe ran but did not return `hello` as expected.", - ...(summary ? { detail: summary.replace(/\s+/g, " ").trim().slice(0, 240) } : {}), ...(hasHello ? {} : { @@ -433,20 +390,20 @@ export async function testEnvironment( }), }); } else { - // Surface the actual failure instead of the leading stream-json - // `system/init` line: the real error lives in the final `result` - // event (parsed) or, when the CLI dies before emitting one, the last - // non-init stdout line — never the first one `summarizeProbeDetail` - // returns. + // Compose the richest raw diagnostic for the log. The real error lives + // in the final `result` event (parsed) or, when the CLI dies before it + // emits one, the last non-init stdout line — never the first line that + // `summarizeProbeDetail` returns. const stdoutFallback = lastNonInitStdoutLine(probe.stdout); const failureDetail = (parsed ? describeClaudeFailure(parsed) : null) || - (firstNonEmptyLine(probe.stderr) - ? truncateDetail(firstNonEmptyLine(probe.stderr)) - : "") || - (stdoutFallback ? truncateDetail(stdoutFallback) : "") || - detail || + firstNonEmptyLine(probe.stderr) || + stdoutFallback || + summarizeProbeDetail(probe.stdout, probe.stderr) || ""; + // The failure diagnostic is untrusted. Route it to the log-only + // boundary and return only a fixed public message and hint. + logRedactedSandboxProbeDiagnostic("Claude CLI hello probe failed", failureDetail); // Provider-quota exhaustion (usage/session limit) is classified // separately from generic transient upstream errors: auth works, the // subscription's usage window is just spent. Surface it as its own @@ -467,7 +424,6 @@ export async function testEnvironment( code: "claude_hello_probe_usage_limited", level: "warn", message: "Claude hello probe hit the subscription usage limit.", - ...(failureDetail ? { detail: failureDetail } : {}), hint: "Authentication works; the account's usage window is exhausted. Wait for the limit to reset and re-run Test.", } : transient @@ -475,14 +431,12 @@ export async function testEnvironment( code: "claude_hello_probe_transient_upstream", level: "warn", message: "Claude hello probe hit a transient upstream error (rate limit or overload).", - ...(failureDetail ? { detail: failureDetail } : {}), hint: "This is usually temporary. Wait a moment and re-run Test.", } : { code: "claude_hello_probe_failed", level: "error", message: "Claude hello probe failed.", - ...(failureDetail ? { detail: failureDetail } : {}), hint: `Exit code ${probe.exitCode ?? "unknown"}. Run \`claude --print - --output-format stream-json --verbose\` manually in this directory and prompt \`Respond with hello\` to debug.`, }, ); diff --git a/packages/db/src/claude-setup-token-sessions-schema.test.ts b/packages/db/src/claude-setup-token-sessions-schema.test.ts new file mode 100644 index 0000000000..b9b72fb6c5 --- /dev/null +++ b/packages/db/src/claude-setup-token-sessions-schema.test.ts @@ -0,0 +1,116 @@ +import fs from "node:fs"; +import { getTableConfig } from "drizzle-orm/pg-core"; +import { describe, expect, it } from "vitest"; +import { claudeSetupTokenSessions } from "./schema/claude_setup_token_sessions.js"; + +type PgTable = Parameters[0]; + +function findIndex(table: PgTable, indexName: string) { + return getTableConfig(table).indexes.find((candidate) => candidate.config.name === indexName); +} + +function indexColumns(table: PgTable, indexName: string): string[] { + const index = findIndex(table, indexName); + if (!index) return []; + return index.config.columns.map((column) => (column as { name: string }).name); +} + +function column(table: PgTable, columnName: string) { + return getTableConfig(table).columns.find((candidate) => candidate.name === columnName); +} + +function columnIsNotNull(table: PgTable, columnName: string): boolean { + return column(table, columnName)?.notNull ?? false; +} + +const ACTIVE_INDEX = "claude_setup_token_sessions_active_uq"; + +// The migration that creates the table. The test locates it by content, so a +// later re-generation with a different name does not break the test. +function activeIndexMigrationSql(): string { + const migrationsDir = new URL("./migrations/", import.meta.url); + const files = fs.readdirSync(migrationsDir).filter((name) => name.endsWith(".sql")); + for (const name of files) { + const content = fs.readFileSync(new URL(name, migrationsDir), "utf8"); + if (content.includes(ACTIVE_INDEX)) { + return content; + } + } + throw new Error(`no migration creates ${ACTIVE_INDEX}`); +} + +describe("claude setup token sessions schema", () => { + it("keeps the scope columns and the deadline non-null", () => { + expect(columnIsNotNull(claudeSetupTokenSessions, "session_id")).toBe(true); + expect(columnIsNotNull(claudeSetupTokenSessions, "company_id")).toBe(true); + expect(columnIsNotNull(claudeSetupTokenSessions, "owner_user_id")).toBe(true); + expect(columnIsNotNull(claudeSetupTokenSessions, "adapter_type")).toBe(true); + expect(columnIsNotNull(claudeSetupTokenSessions, "environment_id")).toBe(true); + expect(columnIsNotNull(claudeSetupTokenSessions, "state")).toBe(true); + expect(columnIsNotNull(claudeSetupTokenSessions, "deadline_at")).toBe(true); + }); + + it("keeps the claim marker and the lease reference nullable", () => { + // A row starts with no claim consumption and no lease. The service fills + // `bound_at` only when the create path consumes the stored claim. + expect(column(claudeSetupTokenSessions, "bound_at")).toBeDefined(); + expect(columnIsNotNull(claudeSetupTokenSessions, "bound_at")).toBe(false); + expect(column(claudeSetupTokenSessions, "lease_id")).toBeDefined(); + expect(columnIsNotNull(claudeSetupTokenSessions, "lease_id")).toBe(false); + }); + + it("references the company and the environment", () => { + const foreignKeys = getTableConfig(claudeSetupTokenSessions).foreignKeys; + const referencedTables = foreignKeys.map((fk) => fk.reference().foreignTable); + const referencedNames = referencedTables.map( + (table) => getTableConfig(table as PgTable).name, + ); + expect(referencedNames).toContain("companies"); + expect(referencedNames).toContain("environments"); + }); + + it("holds no secret-bearing column", () => { + // The durable store keeps only ids, the state, and the deadlines. It never + // holds the login URL, the browser code, the token, or a process chunk. + const names = getTableConfig(claudeSetupTokenSessions).columns.map((c) => c.name); + const forbidden = ["token", "code", "url", "secret", "credential", "prompt", "output"]; + for (const name of names) { + for (const needle of forbidden) { + expect(name.includes(needle)).toBe(false); + } + } + }); + + it("serializes the active session over the full owner scope", () => { + const active = findIndex(claudeSetupTokenSessions, ACTIVE_INDEX); + expect(active).toBeDefined(); + expect(active?.config.unique).toBe(true); + expect(indexColumns(claudeSetupTokenSessions, ACTIVE_INDEX)).toEqual([ + "company_id", + "owner_user_id", + "adapter_type", + "environment_id", + ]); + // The active index is partial; the where clause is present. + expect(active?.config.where).toBeDefined(); + }); + + it("scopes the active partial unique index to the active states only", () => { + const sql = activeIndexMigrationSql(); + expect(sql).toContain(`CREATE UNIQUE INDEX "${ACTIVE_INDEX}"`); + expect(sql).toContain('("company_id","owner_user_id","adapter_type","environment_id")'); + // The active states are in the predicate. + expect(sql).toContain("'starting'"); + expect(sql).toContain("'awaiting_code'"); + expect(sql).toContain("'submitting'"); + expect(sql).toContain("'persisting'"); + // The stored state and the terminal states stay out of the predicate. + const predicate = sql.slice(sql.indexOf(ACTIVE_INDEX)); + const whereClause = predicate.slice(predicate.indexOf("WHERE"), predicate.indexOf(";")); + expect(whereClause).not.toContain("'stored'"); + expect(whereClause).not.toContain("'completed'"); + expect(whereClause).not.toContain("'failed'"); + expect(whereClause).not.toContain("'timed_out'"); + expect(whereClause).not.toContain("'cancelled'"); + }); +}); diff --git a/packages/db/src/migrations/0221_swift_justin_hammer.sql b/packages/db/src/migrations/0221_swift_justin_hammer.sql new file mode 100644 index 0000000000..0fad2812db --- /dev/null +++ b/packages/db/src/migrations/0221_swift_justin_hammer.sql @@ -0,0 +1,20 @@ +CREATE TABLE "claude_setup_token_sessions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "session_id" text NOT NULL, + "company_id" uuid NOT NULL, + "owner_user_id" text NOT NULL, + "adapter_type" text NOT NULL, + "environment_id" uuid NOT NULL, + "lease_id" text, + "state" text DEFAULT 'starting' NOT NULL, + "deadline_at" timestamp with time zone NOT NULL, + "bound_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "claude_setup_token_sessions" ADD CONSTRAINT "claude_setup_token_sessions_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "claude_setup_token_sessions" ADD CONSTRAINT "claude_setup_token_sessions_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "claude_setup_token_sessions_active_uq" ON "claude_setup_token_sessions" USING btree ("company_id","owner_user_id","adapter_type","environment_id") WHERE "claude_setup_token_sessions"."state" IN ('starting', 'awaiting_code', 'submitting', 'persisting');--> statement-breakpoint +CREATE UNIQUE INDEX "claude_setup_token_sessions_session_id_uq" ON "claude_setup_token_sessions" USING btree ("session_id");--> statement-breakpoint +CREATE INDEX "claude_setup_token_sessions_deadline_idx" ON "claude_setup_token_sessions" USING btree ("deadline_at"); diff --git a/packages/db/src/migrations/0222_orphan_cleanup_env_reference_survives_delete.sql b/packages/db/src/migrations/0222_orphan_cleanup_env_reference_survives_delete.sql new file mode 100644 index 0000000000..3e46956577 --- /dev/null +++ b/packages/db/src/migrations/0222_orphan_cleanup_env_reference_survives_delete.sql @@ -0,0 +1,4 @@ +ALTER TABLE "environment_leases" DROP CONSTRAINT "environment_leases_environment_id_environments_id_fk"; +--> statement-breakpoint +ALTER TABLE "environment_leases" ALTER COLUMN "environment_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "environment_leases" ADD CONSTRAINT "environment_leases_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0221_snapshot.json b/packages/db/src/migrations/meta/0221_snapshot.json new file mode 100644 index 0000000000..db920043f8 --- /dev/null +++ b/packages/db/src/migrations/meta/0221_snapshot.json @@ -0,0 +1,39714 @@ +{ + "id": "8f54003b-ed12-4ab6-a3f3-c30b4284bcc7", + "prevId": "66cebac9-8e3b-4060-82e4-21b52490e8ab", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_company_agent_created_idx": { + "name": "activity_log_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_company_responsible_user_created_idx": { + "name": "activity_log_company_responsible_user_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.adapter_auth_sessions": { + "name": "adapter_auth_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'starting'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "promotion_expires_at": { + "name": "promotion_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_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": { + "adapter_auth_sessions_company_status_idx": { + "name": "adapter_auth_sessions_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_company_adapter_active_uq": { + "name": "adapter_auth_sessions_company_adapter_active_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"adapter_auth_sessions\".\"status\" IN ('starting', 'waiting_for_user', 'promoting')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_environment_idx": { + "name": "adapter_auth_sessions_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_expires_idx": { + "name": "adapter_auth_sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_provider_lease_idx": { + "name": "adapter_auth_sessions_provider_lease_idx", + "columns": [ + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "adapter_auth_sessions_company_id_companies_id_fk": { + "name": "adapter_auth_sessions_company_id_companies_id_fk", + "tableFrom": "adapter_auth_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "adapter_auth_sessions_environment_id_environments_id_fk": { + "name": "adapter_auth_sessions_environment_id_environments_id_fk", + "tableFrom": "adapter_auth_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_config": { + "name": "scope_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_memberships": { + "name": "agent_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'joined'" + }, + "starred_at": { + "name": "starred_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": { + "agent_memberships_company_user_idx": { + "name": "agent_memberships_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memberships_company_user_starred_idx": { + "name": "agent_memberships_company_user_starred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memberships_agent_idx": { + "name": "agent_memberships_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memberships_company_user_agent_uq": { + "name": "agent_memberships_company_user_agent_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memberships_company_id_companies_id_fk": { + "name": "agent_memberships_company_id_companies_id_fk", + "tableFrom": "agent_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memberships_agent_id_agents_id_fk": { + "name": "agent_memberships_agent_id_agents_id_fk", + "tableFrom": "agent_memberships", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_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 + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_review_path_recovery_idempotency_uq": { + "name": "agent_wakeup_requests_review_path_recovery_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_wakeup_requests\".\"idempotency_key\" LIKE 'issue_review_path_lost:%' AND \"agent_wakeup_requests\".\"status\" <> 'skipped'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_payload_issue_idx": { + "name": "agent_wakeup_requests_company_payload_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"payload\" ->> 'issueId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_reason": { + "name": "error_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_default_environment_idx": { + "name": "agents_company_default_environment_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "default_environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_default_environment_id_environments_id_fk": { + "name": "agents_default_environment_id_environments_id_fk", + "tableFrom": "agents", + "tableTo": "environments", + "columnsFrom": [ + "default_environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_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": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_api_keys": { + "name": "board_api_keys", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_api_keys_key_hash_idx": { + "name": "board_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_api_keys_user_idx": { + "name": "board_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_api_keys_user_id_user_id_fk": { + "name": "board_api_keys_user_id_user_id_fk", + "tableFrom": "board_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "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 + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.built_in_managed_resources": { + "name": "built_in_managed_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bundle_key": { + "name": "bundle_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_kind": { + "name": "resource_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stock_version": { + "name": "stock_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stock_hash": { + "name": "stock_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "defaults_json": { + "name": "defaults_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": { + "built_in_managed_resources_company_idx": { + "name": "built_in_managed_resources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "built_in_managed_resources_resource_idx": { + "name": "built_in_managed_resources_resource_idx", + "columns": [ + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "built_in_managed_resources_company_bundle_resource_uq": { + "name": "built_in_managed_resources_company_bundle_resource_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bundle_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "built_in_managed_resources_company_id_companies_id_fk": { + "name": "built_in_managed_resources_company_id_companies_id_fk", + "tableFrom": "built_in_managed_resources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.case_attachments": { + "name": "case_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_attachments_company_case_idx": { + "name": "case_attachments_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_attachments_asset_uq": { + "name": "case_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_attachments_company_id_companies_id_fk": { + "name": "case_attachments_company_id_companies_id_fk", + "tableFrom": "case_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_attachments_case_id_cases_id_fk": { + "name": "case_attachments_case_id_cases_id_fk", + "tableFrom": "case_attachments", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_attachments_asset_id_assets_id_fk": { + "name": "case_attachments_asset_id_assets_id_fk", + "tableFrom": "case_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.case_documents": { + "name": "case_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "case_documents_company_case_key_uq": { + "name": "case_documents_company_case_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_documents_document_uq": { + "name": "case_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_documents_company_case_updated_idx": { + "name": "case_documents_company_case_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_documents_company_id_companies_id_fk": { + "name": "case_documents_company_id_companies_id_fk", + "tableFrom": "case_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_documents_case_id_cases_id_fk": { + "name": "case_documents_case_id_cases_id_fk", + "tableFrom": "case_documents", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_documents_document_id_documents_id_fk": { + "name": "case_documents_document_id_documents_id_fk", + "tableFrom": "case_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.case_events": { + "name": "case_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_events_case_created_idx": { + "name": "case_events_case_created_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_events_company_case_idx": { + "name": "case_events_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_events_company_id_companies_id_fk": { + "name": "case_events_company_id_companies_id_fk", + "tableFrom": "case_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_events_case_id_cases_id_fk": { + "name": "case_events_case_id_cases_id_fk", + "tableFrom": "case_events", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_events_actor_agent_id_agents_id_fk": { + "name": "case_events_actor_agent_id_agents_id_fk", + "tableFrom": "case_events", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "case_events_kind_check": { + "name": "case_events_kind_check", + "value": "\"case_events\".\"kind\" in (\n 'created',\n 'updated',\n 'fields_changed',\n 'status_changed',\n 'issue_linked',\n 'issue_unlinked',\n 'document_revised',\n 'child_linked',\n 'attachment_added',\n 'label_added',\n 'label_removed'\n )" + }, + "case_events_actor_type_check": { + "name": "case_events_actor_type_check", + "value": "\"case_events\".\"actor_type\" in ('user', 'agent', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.case_issue_links": { + "name": "case_issue_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_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": { + "case_issue_links_case_issue_uq": { + "name": "case_issue_links_case_issue_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_issue_links_company_case_idx": { + "name": "case_issue_links_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_issue_links_issue_idx": { + "name": "case_issue_links_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_issue_links_company_id_companies_id_fk": { + "name": "case_issue_links_company_id_companies_id_fk", + "tableFrom": "case_issue_links", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_issue_links_case_id_cases_id_fk": { + "name": "case_issue_links_case_id_cases_id_fk", + "tableFrom": "case_issue_links", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_issue_links_issue_id_issues_id_fk": { + "name": "case_issue_links_issue_id_issues_id_fk", + "tableFrom": "case_issue_links", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "case_issue_links_role_check": { + "name": "case_issue_links_role_check", + "value": "\"case_issue_links\".\"role\" in ('origin', 'work', 'reference')" + } + }, + "isRLSEnabled": false + }, + "public.case_labels": { + "name": "case_labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_labels_case_label_uq": { + "name": "case_labels_case_label_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_labels_company_case_idx": { + "name": "case_labels_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_labels_label_idx": { + "name": "case_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_labels_company_id_companies_id_fk": { + "name": "case_labels_company_id_companies_id_fk", + "tableFrom": "case_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_labels_case_id_cases_id_fk": { + "name": "case_labels_case_id_cases_id_fk", + "tableFrom": "case_labels", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_labels_label_id_labels_id_fk": { + "name": "case_labels_label_id_labels_id_fk", + "tableFrom": "case_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cases": { + "name": "cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "case_number": { + "name": "case_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "case_type": { + "name": "case_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "parent_case_id": { + "name": "parent_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "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": { + "cases_company_case_number_uq": { + "name": "cases_company_case_number_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_identifier_uq": { + "name": "cases_identifier_uq", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_type_key_uq": { + "name": "cases_company_type_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_status_idx": { + "name": "cases_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_type_idx": { + "name": "cases_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_project_idx": { + "name": "cases_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_parent_idx": { + "name": "cases_parent_idx", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_title_search_idx": { + "name": "cases_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "cases_identifier_search_idx": { + "name": "cases_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "cases_summary_search_idx": { + "name": "cases_summary_search_idx", + "columns": [ + { + "expression": "summary", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "cases_company_id_companies_id_fk": { + "name": "cases_company_id_companies_id_fk", + "tableFrom": "cases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cases_project_id_projects_id_fk": { + "name": "cases_project_id_projects_id_fk", + "tableFrom": "cases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cases_parent_case_id_cases_id_fk": { + "name": "cases_parent_case_id_cases_id_fk", + "tableFrom": "cases", + "tableTo": "cases", + "columnsFrom": [ + "parent_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cases_created_by_agent_id_agents_id_fk": { + "name": "cases_created_by_agent_id_agents_id_fk", + "tableFrom": "cases", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cases_status_check": { + "name": "cases_status_check", + "value": "\"cases\".\"status\" in ('draft', 'in_progress', 'in_review', 'approved', 'done', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.claude_setup_token_sessions": { + "name": "claude_setup_token_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "lease_id": { + "name": "lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'starting'" + }, + "deadline_at": { + "name": "deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "bound_at": { + "name": "bound_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": { + "claude_setup_token_sessions_active_uq": { + "name": "claude_setup_token_sessions_active_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"claude_setup_token_sessions\".\"state\" IN ('starting', 'awaiting_code', 'submitting', 'persisting')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "claude_setup_token_sessions_session_id_uq": { + "name": "claude_setup_token_sessions_session_id_uq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "claude_setup_token_sessions_deadline_idx": { + "name": "claude_setup_token_sessions_deadline_idx", + "columns": [ + { + "expression": "deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "claude_setup_token_sessions_company_id_companies_id_fk": { + "name": "claude_setup_token_sessions_company_id_companies_id_fk", + "tableFrom": "claude_setup_token_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "claude_setup_token_sessions_environment_id_environments_id_fk": { + "name": "claude_setup_token_sessions_environment_id_environments_id_fk", + "tableFrom": "claude_setup_token_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_auth_challenges": { + "name": "cli_auth_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_access": { + "name": "requested_access", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'board'" + }, + "requested_company_id": { + "name": "requested_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pending_key_hash": { + "name": "pending_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_key_name": { + "name": "pending_key_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_api_key_id": { + "name": "board_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cli_auth_challenges_secret_hash_idx": { + "name": "cli_auth_challenges_secret_hash_idx", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_approved_by_idx": { + "name": "cli_auth_challenges_approved_by_idx", + "columns": [ + { + "expression": "approved_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_requested_company_idx": { + "name": "cli_auth_challenges_requested_company_idx", + "columns": [ + { + "expression": "requested_company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_auth_challenges_requested_company_id_companies_id_fk": { + "name": "cli_auth_challenges_requested_company_id_companies_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "companies", + "columnsFrom": [ + "requested_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_approved_by_user_id_user_id_fk": { + "name": "cli_auth_challenges_approved_by_user_id_user_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "user", + "columnsFrom": [ + "approved_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk": { + "name": "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "board_api_keys", + "columnsFrom": [ + "board_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attachment_max_bytes": { + "name": "attachment_max_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10485760 + }, + "default_responsible_user_id": { + "name": "default_responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "interaction_resolver_governance": { + "name": "interaction_resolver_governance", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "feedback_data_sharing_enabled": { + "name": "feedback_data_sharing_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_consent_at": { + "name": "feedback_data_sharing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_consent_by_user_id": { + "name": "feedback_data_sharing_consent_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_terms_version": { + "name": "feedback_data_sharing_terms_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "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": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "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": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_onboarding_seeds": { + "name": "company_onboarding_seeds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mission": { + "name": "mission", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_role": { + "name": "agent_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_task_title": { + "name": "first_task_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_task_details": { + "name": "first_task_details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_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": { + "company_onboarding_seeds_company_uq": { + "name": "company_onboarding_seeds_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_onboarding_seeds_company_id_companies_id_fk": { + "name": "company_onboarding_seeds_company_id_companies_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_onboarding_seeds_goal_id_goals_id_fk": { + "name": "company_onboarding_seeds_goal_id_goals_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_onboarding_seeds_agent_id_agents_id_fk": { + "name": "company_onboarding_seeds_agent_id_agents_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_onboarding_seeds_issue_id_issues_id_fk": { + "name": "company_onboarding_seeds_issue_id_issues_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_bindings": { + "name": "company_secret_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_selector": { + "name": "version_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'latest'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projection_class": { + "name": "projection_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unclassified'" + }, + "projection_allowlist_key": { + "name": "projection_allowlist_key", + "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": { + "company_secret_bindings_company_idx": { + "name": "company_secret_bindings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_bindings_secret_idx": { + "name": "company_secret_bindings_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_bindings_target_idx": { + "name": "company_secret_bindings_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_bindings_target_path_uq": { + "name": "company_secret_bindings_target_path_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_bindings_company_id_companies_id_fk": { + "name": "company_secret_bindings_company_id_companies_id_fk", + "tableFrom": "company_secret_bindings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secret_bindings_secret_id_company_secrets_id_fk": { + "name": "company_secret_bindings_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_bindings", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_proposals": { + "name": "company_secret_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "proposed_name": { + "name": "proposed_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed_key": { + "name": "proposed_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed_description": { + "name": "proposed_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "justification": { + "name": "justification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_ciphertext": { + "name": "value_ciphertext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "value_fingerprint_sha256": { + "name": "value_fingerprint_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value_length": { + "name": "value_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "secret_proposal_id": { + "name": "secret_proposal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projection_class": { + "name": "projection_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unclassified'" + }, + "binding_target_policy_snapshot": { + "name": "binding_target_policy_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposer_ancestor_ids_snapshot": { + "name": "proposer_ancestor_ids_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_ancestor_ids_snapshot": { + "name": "target_ancestor_ids_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "proposed_by_agent_id": { + "name": "proposed_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_secret_id": { + "name": "created_secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applied_binding_config_path": { + "name": "applied_binding_config_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ciphertext_scrubbed_at": { + "name": "ciphertext_scrubbed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secret_proposals_company_status_idx": { + "name": "company_secret_proposals_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_proposals_proposer_status_idx": { + "name": "company_secret_proposals_proposer_status_idx", + "columns": [ + { + "expression": "proposed_by_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_proposals_expiry_idx": { + "name": "company_secret_proposals_expiry_idx", + "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": {} + }, + "company_secret_proposals_secret_proposal_idx": { + "name": "company_secret_proposals_secret_proposal_idx", + "columns": [ + { + "expression": "secret_proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_proposals_company_id_companies_id_fk": { + "name": "company_secret_proposals_company_id_companies_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secret_proposals_secret_id_company_secrets_id_fk": { + "name": "company_secret_proposals_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secret_proposals_secret_proposal_id_company_secret_proposals_id_fk": { + "name": "company_secret_proposals_secret_proposal_id_company_secret_proposals_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "company_secret_proposals", + "columnsFrom": [ + "secret_proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_target_id_agents_id_fk": { + "name": "company_secret_proposals_target_id_agents_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "agents", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_proposed_by_agent_id_agents_id_fk": { + "name": "company_secret_proposals_proposed_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "agents", + "columnsFrom": [ + "proposed_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_origin_issue_id_issues_id_fk": { + "name": "company_secret_proposals_origin_issue_id_issues_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "issues", + "columnsFrom": [ + "origin_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secret_proposals_origin_run_id_heartbeat_runs_id_fk": { + "name": "company_secret_proposals_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "origin_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_created_secret_id_company_secrets_id_fk": { + "name": "company_secret_proposals_created_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "company_secrets", + "columnsFrom": [ + "created_secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "company_secret_proposals_kind_check": { + "name": "company_secret_proposals_kind_check", + "value": "\"company_secret_proposals\".\"kind\" in ('secret', 'binding')" + }, + "company_secret_proposals_status_check": { + "name": "company_secret_proposals_status_check", + "value": "\"company_secret_proposals\".\"status\" in ('pending', 'approved', 'rejected', 'withdrawn', 'expired')" + }, + "company_secret_proposals_projection_check": { + "name": "company_secret_proposals_projection_check", + "value": "\"company_secret_proposals\".\"projection_class\" = 'unclassified'" + }, + "company_secret_proposals_shape_check": { + "name": "company_secret_proposals_shape_check", + "value": "(\n \"company_secret_proposals\".\"kind\" = 'secret'\n and \"company_secret_proposals\".\"proposed_name\" is not null\n and \"company_secret_proposals\".\"proposed_key\" is not null\n and \"company_secret_proposals\".\"secret_id\" is null\n and \"company_secret_proposals\".\"secret_proposal_id\" is null\n and \"company_secret_proposals\".\"target_type\" is null\n and \"company_secret_proposals\".\"target_id\" is null\n and \"company_secret_proposals\".\"config_path\" is null\n ) or (\n \"company_secret_proposals\".\"kind\" = 'binding'\n and ((\"company_secret_proposals\".\"secret_id\" is not null)::int + (\"company_secret_proposals\".\"secret_proposal_id\" is not null)::int) = 1\n and \"company_secret_proposals\".\"target_type\" = 'agent'\n and \"company_secret_proposals\".\"target_id\" is not null\n and \"company_secret_proposals\".\"config_path\" is not null\n )" + } + }, + "isRLSEnabled": false + }, + "public.company_secret_provider_configs": { + "name": "company_secret_provider_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_checked_at": { + "name": "health_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "health_message": { + "name": "health_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_details": { + "name": "health_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secret_provider_configs_company_idx": { + "name": "company_secret_provider_configs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_provider_configs_company_provider_idx": { + "name": "company_secret_provider_configs_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_provider_configs_default_uq": { + "name": "company_secret_provider_configs_default_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secret_provider_configs\".\"is_default\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_provider_configs_company_id_companies_id_fk": { + "name": "company_secret_provider_configs_company_id_companies_id_fk", + "tableFrom": "company_secret_provider_configs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_provider_configs_created_by_agent_id_agents_id_fk": { + "name": "company_secret_provider_configs_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_provider_configs", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_version_ref": { + "name": "provider_version_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'current'" + }, + "fingerprint_sha256": { + "name": "fingerprint_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotation_job_id": { + "name": "rotation_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_fingerprint_idx": { + "name": "company_secret_versions_fingerprint_idx", + "columns": [ + { + "expression": "fingerprint_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_secret_definition_id": { + "name": "user_secret_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "managed_mode": { + "name": "managed_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip_managed'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config_id": { + "name": "provider_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_resolved_at": { + "name": "last_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_scope_idx": { + "name": "company_secrets_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_owner_idx": { + "name": "company_secrets_company_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_user_definition_owner_idx": { + "name": "company_secrets_user_definition_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_provider_config_idx": { + "name": "company_secrets_provider_config_idx", + "columns": [ + { + "expression": "provider_config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'company' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_key_uq": { + "name": "company_secrets_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'company' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_user_definition_owner_uq": { + "name": "company_secrets_user_definition_owner_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'user' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk": { + "name": "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk", + "tableFrom": "company_secrets", + "tableTo": "user_secret_definitions", + "columnsFrom": [ + "user_secret_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secrets_provider_config_id_company_secret_provider_configs_id_fk": { + "name": "company_secrets_provider_config_id_company_secret_provider_configs_id_fk", + "tableFrom": "company_secrets", + "tableTo": "company_secret_provider_configs", + "columnsFrom": [ + "provider_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "company_secrets_scope_shape_check": { + "name": "company_secrets_scope_shape_check", + "value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n )" + } + }, + "isRLSEnabled": false + }, + "public.company_skill_policies": { + "name": "company_skill_policies", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "default_effect": { + "name": "default_effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rules": { + "name": "rules", + "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": {}, + "foreignKeys": { + "company_skill_policies_company_id_companies_id_fk": { + "name": "company_skill_policies_company_id_companies_id_fk", + "tableFrom": "company_skill_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_comments": { + "name": "company_skill_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_skill_id": { + "name": "company_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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": { + "company_skill_comments_company_skill_created_idx": { + "name": "company_skill_comments_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_comments_parent_idx": { + "name": "company_skill_comments_parent_idx", + "columns": [ + { + "expression": "parent_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_comments_company_id_companies_id_fk": { + "name": "company_skill_comments_company_id_companies_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_comments_company_skill_id_company_skills_id_fk": { + "name": "company_skill_comments_company_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "company_skills", + "columnsFrom": [ + "company_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_comments_parent_comment_id_company_skill_comments_id_fk": { + "name": "company_skill_comments_parent_comment_id_company_skill_comments_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "company_skill_comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skill_comments_author_agent_id_agents_id_fk": { + "name": "company_skill_comments_author_agent_id_agents_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_stars": { + "name": "company_skill_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_skill_id": { + "name": "company_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "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": { + "company_skill_stars_skill_agent_idx": { + "name": "company_skill_stars_skill_agent_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_stars_skill_user_idx": { + "name": "company_skill_stars_skill_user_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_stars_company_skill_created_idx": { + "name": "company_skill_stars_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_stars_company_id_companies_id_fk": { + "name": "company_skill_stars_company_id_companies_id_fk", + "tableFrom": "company_skill_stars", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_stars_company_skill_id_company_skills_id_fk": { + "name": "company_skill_stars_company_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_stars", + "tableTo": "company_skills", + "columnsFrom": [ + "company_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_stars_agent_id_agents_id_fk": { + "name": "company_skill_stars_agent_id_agents_id_fk", + "tableFrom": "company_skill_stars", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_test_inputs": { + "name": "company_skill_test_inputs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_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 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "company_skill_test_inputs_company_skill_name_idx": { + "name": "company_skill_test_inputs_company_skill_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_inputs_company_skill_active_idx": { + "name": "company_skill_test_inputs_company_skill_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_test_inputs_company_id_companies_id_fk": { + "name": "company_skill_test_inputs_company_id_companies_id_fk", + "tableFrom": "company_skill_test_inputs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_inputs_skill_id_company_skills_id_fk": { + "name": "company_skill_test_inputs_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_test_inputs", + "tableTo": "company_skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_test_run_templates": { + "name": "company_skill_test_run_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_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 + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "company_skill_test_run_templates_company_active_idx": { + "name": "company_skill_test_run_templates_company_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_test_run_templates_company_id_companies_id_fk": { + "name": "company_skill_test_run_templates_company_id_companies_id_fk", + "tableFrom": "company_skill_test_run_templates", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_run_templates_created_by_agent_id_agents_id_fk": { + "name": "company_skill_test_run_templates_created_by_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_run_templates", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skill_test_run_templates_updated_by_agent_id_agents_id_fk": { + "name": "company_skill_test_run_templates_updated_by_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_run_templates", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_test_runs": { + "name": "company_skill_test_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "input_id": { + "name": "input_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "input_snapshot": { + "name": "input_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "skill_version_id": { + "name": "skill_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_config_snapshot": { + "name": "agent_config_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_name": { + "name": "template_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_body": { + "name": "template_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_template_body": { + "name": "rendered_template_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness_issue_description": { + "name": "harness_issue_description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "output_document_key": { + "name": "output_document_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'output'" + }, + "output_snapshot": { + "name": "output_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "harness_issue_expires_at": { + "name": "harness_issue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "harness_issue_deleted_at": { + "name": "harness_issue_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": { + "company_skill_test_runs_company_skill_created_idx": { + "name": "company_skill_test_runs_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_issue_idx": { + "name": "company_skill_test_runs_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_input_created_idx": { + "name": "company_skill_test_runs_company_input_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "input_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_status_idx": { + "name": "company_skill_test_runs_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_harness_expires_idx": { + "name": "company_skill_test_runs_company_harness_expires_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "harness_issue_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_test_runs_company_id_companies_id_fk": { + "name": "company_skill_test_runs_company_id_companies_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_runs_skill_id_company_skills_id_fk": { + "name": "company_skill_test_runs_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "company_skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_runs_input_id_company_skill_test_inputs_id_fk": { + "name": "company_skill_test_runs_input_id_company_skill_test_inputs_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "company_skill_test_inputs", + "columnsFrom": [ + "input_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skill_test_runs_skill_version_id_company_skill_versions_id_fk": { + "name": "company_skill_test_runs_skill_version_id_company_skill_versions_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "company_skill_versions", + "columnsFrom": [ + "skill_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "company_skill_test_runs_agent_id_agents_id_fk": { + "name": "company_skill_test_runs_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "company_skill_test_runs_issue_id_issues_id_fk": { + "name": "company_skill_test_runs_issue_id_issues_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_versions": { + "name": "company_skill_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_skill_id": { + "name": "company_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_name": { + "name": "release_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_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": { + "company_skill_versions_skill_revision_idx": { + "name": "company_skill_versions_skill_revision_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_versions_skill_release_idx": { + "name": "company_skill_versions_skill_release_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_skill_versions\".\"release_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_versions_company_skill_created_idx": { + "name": "company_skill_versions_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_versions_company_id_companies_id_fk": { + "name": "company_skill_versions_company_id_companies_id_fk", + "tableFrom": "company_skill_versions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_versions_company_skill_id_company_skills_id_fk": { + "name": "company_skill_versions_company_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_versions", + "tableTo": "company_skills", + "columnsFrom": [ + "company_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_versions_author_agent_id_agents_id_fk": { + "name": "company_skill_versions_author_agent_id_agents_id_fk", + "tableFrom": "company_skill_versions", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skills": { + "name": "company_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "source_locator": { + "name": "source_locator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_level": { + "name": "trust_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown_only'" + }, + "compatibility": { + "name": "compatibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compatible'" + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "homepage_url": { + "name": "homepage_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sharing_scope": { + "name": "sharing_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "public_share_token": { + "name": "public_share_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "forked_from_company_id": { + "name": "forked_from_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "star_count": { + "name": "star_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "install_count": { + "name": "install_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fork_count": { + "name": "fork_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "current_version_id": { + "name": "current_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skills_company_key_idx": { + "name": "company_skills_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_name_idx": { + "name": "company_skills_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_folder_idx": { + "name": "company_skills_company_folder_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_categories_idx": { + "name": "company_skills_company_categories_idx", + "columns": [ + { + "expression": "categories", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "company_skills_company_sharing_scope_idx": { + "name": "company_skills_company_sharing_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sharing_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_current_version_idx": { + "name": "company_skills_company_current_version_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_forked_from_idx": { + "name": "company_skills_company_forked_from_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "forked_from_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_skills_folder_id_folders_id_fk": { + "name": "company_skills_folder_id_folders_id_fk", + "tableFrom": "company_skills", + "tableTo": "folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skills_forked_from_skill_id_company_skills_id_fk": { + "name": "company_skills_forked_from_skill_id_company_skills_id_fk", + "tableFrom": "company_skills", + "tableTo": "company_skills", + "columnsFrom": [ + "forked_from_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skills_forked_from_company_id_companies_id_fk": { + "name": "company_skills_forked_from_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "forked_from_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skills_current_version_id_company_skill_versions_id_fk": { + "name": "company_skills_current_version_id_company_skill_versions_id_fk", + "tableFrom": "company_skills", + "tableTo": "company_skill_versions", + "columnsFrom": [ + "current_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_transfer_runs": { + "name": "company_transfer_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "actor_key": { + "name": "actor_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_ref": { + "name": "container_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest_sha256": { + "name": "manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "blob_count": { + "name": "blob_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_parts": { + "name": "completed_parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_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": { + "company_transfer_runs_company_idx": { + "name": "company_transfer_runs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_transfer_runs_idempotency_direction_idx": { + "name": "company_transfer_runs_idempotency_direction_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_transfer_runs_actor_status_idx": { + "name": "company_transfer_runs_actor_status_idx", + "columns": [ + { + "expression": "actor_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_transfer_runs_company_id_companies_id_fk": { + "name": "company_transfer_runs_company_id_companies_id_fk", + "tableFrom": "company_transfer_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_user_sidebar_preferences": { + "name": "company_user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_order": { + "name": "project_order", + "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": { + "company_user_sidebar_preferences_company_idx": { + "name": "company_user_sidebar_preferences_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_user_idx": { + "name": "company_user_sidebar_preferences_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_company_user_uq": { + "name": "company_user_sidebar_preferences_company_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_user_sidebar_preferences_company_id_companies_id_fk": { + "name": "company_user_sidebar_preferences_company_id_companies_id_fk", + "tableFrom": "company_user_sidebar_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "cost_status": { + "name": "cost_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reported'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "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": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_archive_notification_outbox": { + "name": "decision_archive_notification_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archive_version": { + "name": "archive_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "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 + }, + "last_attempt_at": { + "name": "last_attempt_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": { + "decision_archive_notification_outbox_uq": { + "name": "decision_archive_notification_outbox_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archive_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_archive_notification_outbox_pending_idx": { + "name": "decision_archive_notification_outbox_pending_idx", + "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": { + "decision_archive_notification_outbox_company_id_companies_id_fk": { + "name": "decision_archive_notification_outbox_company_id_companies_id_fk", + "tableFrom": "decision_archive_notification_outbox", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_archive_notification_outbox_origin_agent_id_agents_id_fk": { + "name": "decision_archive_notification_outbox_origin_agent_id_agents_id_fk", + "tableFrom": "decision_archive_notification_outbox", + "tableTo": "agents", + "columnsFrom": [ + "origin_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_archive_notification_outbox_status_check": { + "name": "decision_archive_notification_outbox_status_check", + "value": "\"decision_archive_notification_outbox\".\"status\" IN ('pending', 'delivering', 'delivered')" + } + }, + "isRLSEnabled": false + }, + "public.decision_queue_items": { + "name": "decision_queue_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_type": { + "name": "added_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_agent_id": { + "name": "added_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by_run_id": { + "name": "added_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "added_by_agent_api_key_id": { + "name": "added_by_agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_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": { + "decision_queue_items_queue_source_uq": { + "name": "decision_queue_items_queue_source_uq", + "columns": [ + { + "expression": "queue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_queue_items_company_source_idx": { + "name": "decision_queue_items_company_source_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_queue_items_company_id_companies_id_fk": { + "name": "decision_queue_items_company_id_companies_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_queue_items_added_by_agent_id_agents_id_fk": { + "name": "decision_queue_items_added_by_agent_id_agents_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "agents", + "columnsFrom": [ + "added_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queue_items_added_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_queue_items_added_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "added_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queue_items_added_by_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_queue_items_added_by_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "added_by_agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queue_items_queue_company_fk": { + "name": "decision_queue_items_queue_company_fk", + "tableFrom": "decision_queue_items", + "tableTo": "decision_queues", + "columnsFrom": [ + "queue_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_queue_items_actor_check": { + "name": "decision_queue_items_actor_check", + "value": "(\n (\"decision_queue_items\".\"added_by_type\" = 'agent' AND \"decision_queue_items\".\"added_by_agent_id\" IS NOT NULL AND \"decision_queue_items\".\"added_by_user_id\" IS NULL)\n OR (\"decision_queue_items\".\"added_by_type\" = 'user' AND \"decision_queue_items\".\"added_by_agent_id\" IS NULL AND \"decision_queue_items\".\"added_by_user_id\" IS NOT NULL)\n OR (\"decision_queue_items\".\"added_by_type\" = 'system' AND \"decision_queue_items\".\"added_by_agent_id\" IS NULL AND \"decision_queue_items\".\"added_by_user_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_queues": { + "name": "decision_queues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_type": { + "name": "created_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_api_key_id": { + "name": "created_by_agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retention_days": { + "name": "retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seed_rules": { + "name": "seed_rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "seed_rules_enabled": { + "name": "seed_rules_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": { + "decision_queues_company_key_uq": { + "name": "decision_queues_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_queues_company_updated_idx": { + "name": "decision_queues_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_queues_company_id_companies_id_fk": { + "name": "decision_queues_company_id_companies_id_fk", + "tableFrom": "decision_queues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_queues_created_by_agent_id_agents_id_fk": { + "name": "decision_queues_created_by_agent_id_agents_id_fk", + "tableFrom": "decision_queues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queues_created_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_queues_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_queues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queues_created_by_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_queues_created_by_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_queues", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "created_by_agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "decision_queues_id_company_uq": { + "name": "decision_queues_id_company_uq", + "nullsNotDistinct": false, + "columns": [ + "id", + "company_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "decision_queues_creator_check": { + "name": "decision_queues_creator_check", + "value": "(\n (\"decision_queues\".\"created_by_type\" = 'agent' AND \"decision_queues\".\"created_by_agent_id\" IS NOT NULL AND \"decision_queues\".\"created_by_user_id\" IS NULL)\n OR (\"decision_queues\".\"created_by_type\" = 'user' AND \"decision_queues\".\"created_by_agent_id\" IS NULL AND \"decision_queues\".\"created_by_user_id\" IS NOT NULL)\n OR (\"decision_queues\".\"created_by_type\" = 'system' AND \"decision_queues\".\"created_by_agent_id\" IS NULL AND \"decision_queues\".\"created_by_user_id\" IS NULL)\n )" + }, + "decision_queues_retention_days_check": { + "name": "decision_queues_retention_days_check", + "value": "\"decision_queues\".\"retention_days\" IS NULL OR (\"decision_queues\".\"retention_days\" >= 1 AND \"decision_queues\".\"retention_days\" <= 3650)" + } + }, + "isRLSEnabled": false + }, + "public.decision_retention": { + "name": "decision_retention", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_activity_at": { + "name": "source_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_reason": { + "name": "archived_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_type": { + "name": "archived_by_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_agent_id": { + "name": "archived_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_by_user_id": { + "name": "archived_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_run_id": { + "name": "archived_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "archive_version": { + "name": "archive_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": { + "decision_retention_company_source_uq": { + "name": "decision_retention_company_source_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_retention_company_archived_idx": { + "name": "decision_retention_company_archived_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_retention_company_id_companies_id_fk": { + "name": "decision_retention_company_id_companies_id_fk", + "tableFrom": "decision_retention", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_retention_archived_by_agent_id_agents_id_fk": { + "name": "decision_retention_archived_by_agent_id_agents_id_fk", + "tableFrom": "decision_retention", + "tableTo": "agents", + "columnsFrom": [ + "archived_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_retention_archived_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_retention_archived_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_retention", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "archived_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_retention_archive_actor_check": { + "name": "decision_retention_archive_actor_check", + "value": "(\n (\"decision_retention\".\"archived_at\" IS NULL AND \"decision_retention\".\"archived_by_type\" IS NULL AND \"decision_retention\".\"archived_by_agent_id\" IS NULL AND \"decision_retention\".\"archived_by_user_id\" IS NULL)\n OR (\"decision_retention\".\"archived_at\" IS NOT NULL AND \"decision_retention\".\"archived_by_type\" = 'system' AND \"decision_retention\".\"archived_by_agent_id\" IS NULL AND \"decision_retention\".\"archived_by_user_id\" IS NULL)\n OR (\"decision_retention\".\"archived_at\" IS NOT NULL AND \"decision_retention\".\"archived_by_type\" = 'agent' AND \"decision_retention\".\"archived_by_agent_id\" IS NOT NULL AND \"decision_retention\".\"archived_by_user_id\" IS NULL)\n OR (\"decision_retention\".\"archived_at\" IS NOT NULL AND \"decision_retention\".\"archived_by_type\" = 'user' AND \"decision_retention\".\"archived_by_agent_id\" IS NULL AND \"decision_retention\".\"archived_by_user_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_triage": { + "name": "decision_triage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decide_by": { + "name": "decide_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decide_by_date": { + "name": "decide_by_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "set_by_type": { + "name": "set_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "set_by_agent_id": { + "name": "set_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "set_by_user_id": { + "name": "set_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "set_by_run_id": { + "name": "set_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "set_by_agent_api_key_id": { + "name": "set_by_agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_triage_company_source_uq": { + "name": "decision_triage_company_source_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_triage_company_decide_by_idx": { + "name": "decision_triage_company_decide_by_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decide_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_triage_company_id_companies_id_fk": { + "name": "decision_triage_company_id_companies_id_fk", + "tableFrom": "decision_triage", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_triage_set_by_agent_id_agents_id_fk": { + "name": "decision_triage_set_by_agent_id_agents_id_fk", + "tableFrom": "decision_triage", + "tableTo": "agents", + "columnsFrom": [ + "set_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_set_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_triage_set_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_triage", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "set_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_set_by_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_triage_set_by_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_triage", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "set_by_agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_triage_actor_check": { + "name": "decision_triage_actor_check", + "value": "(\n (\"decision_triage\".\"set_by_type\" = 'agent' AND \"decision_triage\".\"set_by_agent_id\" IS NOT NULL AND \"decision_triage\".\"set_by_user_id\" IS NULL)\n OR (\"decision_triage\".\"set_by_type\" = 'user' AND \"decision_triage\".\"set_by_agent_id\" IS NULL AND \"decision_triage\".\"set_by_user_id\" IS NOT NULL)\n )" + }, + "decision_triage_decide_by_check": { + "name": "decision_triage_decide_by_check", + "value": "(\n (\"decision_triage\".\"decide_by\" IS NULL AND \"decision_triage\".\"decide_by_date\" IS NULL)\n OR (\"decision_triage\".\"decide_by\" IN ('today', 'this_week', 'whenever') AND \"decision_triage\".\"decide_by_date\" IS NULL)\n OR (\"decision_triage\".\"decide_by\" = 'date' AND \"decision_triage\".\"decide_by_date\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_triage_events": { + "name": "decision_triage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_run_id": { + "name": "actor_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_api_key_id": { + "name": "agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "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": { + "decision_triage_events_company_source_created_idx": { + "name": "decision_triage_events_company_source_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_triage_events_queue_created_idx": { + "name": "decision_triage_events_queue_created_idx", + "columns": [ + { + "expression": "queue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_triage_events_company_id_companies_id_fk": { + "name": "decision_triage_events_company_id_companies_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_triage_events_queue_id_decision_queues_id_fk": { + "name": "decision_triage_events_queue_id_decision_queues_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "decision_queues", + "columnsFrom": [ + "queue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_events_actor_agent_id_agents_id_fk": { + "name": "decision_triage_events_actor_agent_id_agents_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_events_actor_run_id_heartbeat_runs_id_fk": { + "name": "decision_triage_events_actor_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "actor_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_events_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_triage_events_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_triage_events_actor_check": { + "name": "decision_triage_events_actor_check", + "value": "(\n (\"decision_triage_events\".\"actor_type\" = 'agent' AND \"decision_triage_events\".\"actor_agent_id\" IS NOT NULL AND \"decision_triage_events\".\"actor_user_id\" IS NULL)\n OR (\"decision_triage_events\".\"actor_type\" = 'user' AND \"decision_triage_events\".\"actor_agent_id\" IS NULL AND \"decision_triage_events\".\"actor_user_id\" IS NOT NULL)\n OR (\"decision_triage_events\".\"actor_type\" = 'system' AND \"decision_triage_events\".\"actor_agent_id\" IS NULL AND \"decision_triage_events\".\"actor_user_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_training_examples": { + "name": "decision_training_examples", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cutoff_at": { + "name": "cutoff_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "notes_history": { + "name": "notes_history", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "decision_outcome": { + "name": "decision_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scrub_deleted_comments_v1'" + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_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": { + "decision_training_examples_company_created_at_idx": { + "name": "decision_training_examples_company_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_training_examples_issue_idx": { + "name": "decision_training_examples_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_training_examples_source_author_uq": { + "name": "decision_training_examples_source_author_uq", + "columns": [ + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_training_examples_company_id_companies_id_fk": { + "name": "decision_training_examples_company_id_companies_id_fk", + "tableFrom": "decision_training_examples", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_training_examples_issue_id_issues_id_fk": { + "name": "decision_training_examples_issue_id_issues_id_fk", + "tableFrom": "decision_training_examples", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_bundles": { + "name": "decision_bundles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_bundles_company_created_at_idx": { + "name": "decision_bundles_company_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_bundles_company_id_companies_id_fk": { + "name": "decision_bundles_company_id_companies_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_bundles_origin_agent_id_agents_id_fk": { + "name": "decision_bundles_origin_agent_id_agents_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "agents", + "columnsFrom": [ + "origin_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_bundles_origin_issue_id_issues_id_fk": { + "name": "decision_bundles_origin_issue_id_issues_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "issues", + "columnsFrom": [ + "origin_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_bundles_origin_run_id_heartbeat_runs_id_fk": { + "name": "decision_bundles_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "origin_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_effect_executions": { + "name": "decision_effect_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effect_index": { + "name": "effect_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "effect_type": { + "name": "effect_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_issue_id": { + "name": "target_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claimed'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_log_id": { + "name": "activity_log_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "decision_effect_executions_decision_effect_uq": { + "name": "decision_effect_executions_decision_effect_uq", + "columns": [ + { + "expression": "decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effect_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_effect_executions_target_issue_idx": { + "name": "decision_effect_executions_target_issue_idx", + "columns": [ + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_effect_executions_decision_id_decisions_id_fk": { + "name": "decision_effect_executions_decision_id_decisions_id_fk", + "tableFrom": "decision_effect_executions", + "tableTo": "decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_effect_executions_target_issue_id_issues_id_fk": { + "name": "decision_effect_executions_target_issue_id_issues_id_fk", + "tableFrom": "decision_effect_executions", + "tableTo": "issues", + "columnsFrom": [ + "target_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_effect_executions_activity_log_id_activity_log_id_fk": { + "name": "decision_effect_executions_activity_log_id_activity_log_id_fk", + "tableFrom": "decision_effect_executions", + "tableTo": "activity_log", + "columnsFrom": [ + "activity_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_target_issues": { + "name": "decision_target_issues", + "schema": "", + "columns": { + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "decision_target_issues_decision_idx": { + "name": "decision_target_issues_decision_idx", + "columns": [ + { + "expression": "decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_target_issues_issue_idx": { + "name": "decision_target_issues_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_target_issues_decision_id_decisions_id_fk": { + "name": "decision_target_issues_decision_id_decisions_id_fk", + "tableFrom": "decision_target_issues", + "tableTo": "decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_target_issues_issue_id_issues_id_fk": { + "name": "decision_target_issues_issue_id_issues_id_fk", + "tableFrom": "decision_target_issues", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_target_issues_company_id_companies_id_fk": { + "name": "decision_target_issues_company_id_companies_id_fk", + "tableFrom": "decision_target_issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "decision_target_issues_decision_id_issue_id_pk": { + "name": "decision_target_issues_decision_id_issue_id_pk", + "columns": [ + "decision_id", + "issue_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decisions": { + "name": "decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bundle_id": { + "name": "bundle_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rule_key": { + "name": "rule_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "execution_status": { + "name": "execution_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chosen_option_id": { + "name": "chosen_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_values": { + "name": "input_values", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signed_spec": { + "name": "signed_spec", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_snapshots": { + "name": "target_snapshots", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "continuation_policy": { + "name": "continuation_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "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": { + "decisions_company_status_expires_at_idx": { + "name": "decisions_company_status_expires_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_bundle_idx": { + "name": "decisions_bundle_idx", + "columns": [ + { + "expression": "bundle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_origin_issue_idx": { + "name": "decisions_origin_issue_idx", + "columns": [ + { + "expression": "origin_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_company_idempotency_uq": { + "name": "decisions_company_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"decisions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decisions_company_id_companies_id_fk": { + "name": "decisions_company_id_companies_id_fk", + "tableFrom": "decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_bundle_id_decision_bundles_id_fk": { + "name": "decisions_bundle_id_decision_bundles_id_fk", + "tableFrom": "decisions", + "tableTo": "decision_bundles", + "columnsFrom": [ + "bundle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "decisions_origin_agent_id_agents_id_fk": { + "name": "decisions_origin_agent_id_agents_id_fk", + "tableFrom": "decisions", + "tableTo": "agents", + "columnsFrom": [ + "origin_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_origin_issue_id_issues_id_fk": { + "name": "decisions_origin_issue_id_issues_id_fk", + "tableFrom": "decisions", + "tableTo": "issues", + "columnsFrom": [ + "origin_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_origin_run_id_heartbeat_runs_id_fk": { + "name": "decisions_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "origin_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_annotation_anchor_snapshots": { + "name": "document_annotation_anchor_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_revision_id": { + "name": "from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "from_revision_number": { + "name": "from_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "to_revision_id": { + "name": "to_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_revision_number": { + "name": "to_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_anchor": { + "name": "previous_anchor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "next_anchor": { + "name": "next_anchor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "anchor_state": { + "name": "anchor_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_confidence": { + "name": "anchor_confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_annotation_anchor_snapshots_company_thread_created_at_idx": { + "name": "document_annotation_anchor_snapshots_company_thread_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_anchor_snapshots_company_document_revision_idx": { + "name": "document_annotation_anchor_snapshots_company_document_revision_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_annotation_anchor_snapshots_company_id_companies_id_fk": { + "name": "document_annotation_anchor_snapshots_company_id_companies_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_thread_id_document_annotation_threads_id_fk": { + "name": "document_annotation_anchor_snapshots_thread_id_document_annotation_threads_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "document_annotation_threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_document_id_documents_id_fk": { + "name": "document_annotation_anchor_snapshots_document_id_documents_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_from_revision_id_document_revisions_id_fk": { + "name": "document_annotation_anchor_snapshots_from_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "document_revisions", + "columnsFrom": [ + "from_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_to_revision_id_document_revisions_id_fk": { + "name": "document_annotation_anchor_snapshots_to_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "document_revisions", + "columnsFrom": [ + "to_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_annotation_comments": { + "name": "document_annotation_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_type": { + "name": "author_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "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": { + "document_annotation_comments_company_thread_created_at_idx": { + "name": "document_annotation_comments_company_thread_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_issue_created_at_idx": { + "name": "document_annotation_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_routine_created_at_idx": { + "name": "document_annotation_comments_company_routine_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_case_created_at_idx": { + "name": "document_annotation_comments_company_case_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_document_created_at_idx": { + "name": "document_annotation_comments_company_document_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_issue_comment_idx": { + "name": "document_annotation_comments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_body_search_idx": { + "name": "document_annotation_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "document_annotation_comments_company_id_companies_id_fk": { + "name": "document_annotation_comments_company_id_companies_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_annotation_comments_thread_id_document_annotation_threads_id_fk": { + "name": "document_annotation_comments_thread_id_document_annotation_threads_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "document_annotation_threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_issue_id_issues_id_fk": { + "name": "document_annotation_comments_issue_id_issues_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_routine_id_routines_id_fk": { + "name": "document_annotation_comments_routine_id_routines_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_case_id_cases_id_fk": { + "name": "document_annotation_comments_case_id_cases_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_document_id_documents_id_fk": { + "name": "document_annotation_comments_document_id_documents_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_author_agent_id_agents_id_fk": { + "name": "document_annotation_comments_author_agent_id_agents_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_annotation_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_comments_issue_comment_id_issue_comments_id_fk": { + "name": "document_annotation_comments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_annotation_comments_exactly_one_owner_chk": { + "name": "document_annotation_comments_exactly_one_owner_chk", + "value": "num_nonnulls(\"document_annotation_comments\".\"issue_id\", \"document_annotation_comments\".\"routine_id\", \"document_annotation_comments\".\"case_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.document_annotation_threads": { + "name": "document_annotation_threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "anchor_state": { + "name": "anchor_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "original_revision_id": { + "name": "original_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_revision_number": { + "name": "original_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "current_revision_number": { + "name": "current_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "selected_text": { + "name": "selected_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix_text": { + "name": "prefix_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "suffix_text": { + "name": "suffix_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "normalized_start": { + "name": "normalized_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "normalized_end": { + "name": "normalized_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "markdown_start": { + "name": "markdown_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "markdown_end": { + "name": "markdown_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "anchor_confidence": { + "name": "anchor_confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exact'" + }, + "anchor_selector": { + "name": "anchor_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "document_annotation_threads_company_document_status_idx": { + "name": "document_annotation_threads_company_document_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_issue_status_idx": { + "name": "document_annotation_threads_company_issue_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_routine_status_idx": { + "name": "document_annotation_threads_company_routine_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_case_status_idx": { + "name": "document_annotation_threads_company_case_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_current_revision_open_idx": { + "name": "document_annotation_threads_company_current_revision_open_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_anchor_state_idx": { + "name": "document_annotation_threads_company_anchor_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "anchor_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_annotation_threads_company_id_companies_id_fk": { + "name": "document_annotation_threads_company_id_companies_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_annotation_threads_issue_id_issues_id_fk": { + "name": "document_annotation_threads_issue_id_issues_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_routine_id_routines_id_fk": { + "name": "document_annotation_threads_routine_id_routines_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_case_id_cases_id_fk": { + "name": "document_annotation_threads_case_id_cases_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_document_id_documents_id_fk": { + "name": "document_annotation_threads_document_id_documents_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_original_revision_id_document_revisions_id_fk": { + "name": "document_annotation_threads_original_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "document_revisions", + "columnsFrom": [ + "original_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_threads_current_revision_id_document_revisions_id_fk": { + "name": "document_annotation_threads_current_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "document_revisions", + "columnsFrom": [ + "current_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_threads_created_by_agent_id_agents_id_fk": { + "name": "document_annotation_threads_created_by_agent_id_agents_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_threads_resolved_by_agent_id_agents_id_fk": { + "name": "document_annotation_threads_resolved_by_agent_id_agents_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_annotation_threads_exactly_one_owner_chk": { + "name": "document_annotation_threads_exactly_one_owner_chk", + "value": "num_nonnulls(\"document_annotation_threads\".\"issue_id\", \"document_annotation_threads\".\"routine_id\", \"document_annotation_threads\".\"case_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.document_memberships": { + "name": "document_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_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": { + "document_memberships_company_user_starred_idx": { + "name": "document_memberships_company_user_starred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_memberships_company_user_document_uq": { + "name": "document_memberships_company_user_document_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_memberships_company_id_companies_id_fk": { + "name": "document_memberships_company_id_companies_id_fk", + "tableFrom": "document_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_memberships_document_id_documents_id_fk": { + "name": "document_memberships_document_id_documents_id_fk", + "tableFrom": "document_memberships", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by_agent_id": { + "name": "locked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "locked_by_user_id": { + "name": "locked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "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": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_title_search_idx": { + "name": "documents_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "documents_latest_body_search_idx": { + "name": "documents_latest_body_search_idx", + "columns": [ + { + "expression": "latest_body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_locked_by_agent_id_agents_id_fk": { + "name": "documents_locked_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "locked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_custom_image_setup_sessions": { + "name": "environment_custom_image_setup_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "promoted_template_id": { + "name": "promoted_template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_lease_id": { + "name": "environment_lease_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'starting'" + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_by_agent_id": { + "name": "started_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "base_template_ref": { + "name": "base_template_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_summary": { + "name": "connection_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "connection_secret_ref": { + "name": "connection_secret_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_custom_image_setup_sessions_environment_status_idx": { + "name": "environment_custom_image_setup_sessions_environment_status_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_environment_active_uq": { + "name": "environment_custom_image_setup_sessions_environment_active_uq", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environment_custom_image_setup_sessions\".\"status\" IN ('starting', 'waiting_for_user', 'capturing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_template_idx": { + "name": "environment_custom_image_setup_sessions_template_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_promoted_template_idx": { + "name": "environment_custom_image_setup_sessions_promoted_template_idx", + "columns": [ + { + "expression": "promoted_template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_expires_idx": { + "name": "environment_custom_image_setup_sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_provider_lease_idx": { + "name": "environment_custom_image_setup_sessions_provider_lease_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_custom_image_setup_sessions_environment_id_environments_id_fk": { + "name": "environment_custom_image_setup_sessions_environment_id_environments_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_template_id_environment_custom_image_templates_id_fk": { + "name": "environment_custom_image_setup_sessions_template_id_environment_custom_image_templates_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environment_custom_image_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_promoted_template_id_environment_custom_image_templates_id_fk": { + "name": "environment_custom_image_setup_sessions_promoted_template_id_environment_custom_image_templates_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environment_custom_image_templates", + "columnsFrom": [ + "promoted_template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_environment_lease_id_environment_leases_id_fk": { + "name": "environment_custom_image_setup_sessions_environment_lease_id_environment_leases_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environment_leases", + "columnsFrom": [ + "environment_lease_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_started_by_agent_id_agents_id_fk": { + "name": "environment_custom_image_setup_sessions_started_by_agent_id_agents_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "agents", + "columnsFrom": [ + "started_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_custom_image_templates": { + "name": "environment_custom_image_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_kind": { + "name": "template_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "template_ref": { + "name": "template_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_template_ref": { + "name": "source_template_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_environment_config_fingerprint": { + "name": "source_environment_config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "captured_at": { + "name": "captured_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 + }, + "superseded_by_template_id": { + "name": "superseded_by_template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_custom_image_templates_environment_status_idx": { + "name": "environment_custom_image_templates_environment_status_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_environment_provider_status_idx": { + "name": "environment_custom_image_templates_environment_provider_status_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_environment_active_uq": { + "name": "environment_custom_image_templates_environment_active_uq", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environment_custom_image_templates\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_superseded_by_idx": { + "name": "environment_custom_image_templates_superseded_by_idx", + "columns": [ + { + "expression": "superseded_by_template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_last_used_idx": { + "name": "environment_custom_image_templates_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_custom_image_templates_environment_id_environments_id_fk": { + "name": "environment_custom_image_templates_environment_id_environments_id_fk", + "tableFrom": "environment_custom_image_templates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_custom_image_templates_created_by_agent_id_agents_id_fk": { + "name": "environment_custom_image_templates_created_by_agent_id_agents_id_fk", + "tableFrom": "environment_custom_image_templates", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_templates_superseded_by_template_id_environment_custom_image_templates_id_fk": { + "name": "environment_custom_image_templates_superseded_by_template_id_environment_custom_image_templates_id_fk", + "tableFrom": "environment_custom_image_templates", + "tableTo": "environment_custom_image_templates", + "columnsFrom": [ + "superseded_by_template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_leases": { + "name": "environment_leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "lease_policy": { + "name": "lease_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ephemeral'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acquired_at": { + "name": "acquired_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": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_status": { + "name": "cleanup_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_leases_company_environment_status_idx": { + "name": "environment_leases_company_environment_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_execution_workspace_idx": { + "name": "environment_leases_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_issue_idx": { + "name": "environment_leases_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_heartbeat_run_idx": { + "name": "environment_leases_heartbeat_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_last_used_idx": { + "name": "environment_leases_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_provider_lease_idx": { + "name": "environment_leases_provider_lease_idx", + "columns": [ + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_leases_company_id_companies_id_fk": { + "name": "environment_leases_company_id_companies_id_fk", + "tableFrom": "environment_leases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_leases_environment_id_environments_id_fk": { + "name": "environment_leases_environment_id_environments_id_fk", + "tableFrom": "environment_leases", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_leases_execution_workspace_id_execution_workspaces_id_fk": { + "name": "environment_leases_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "environment_leases", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_issue_id_issues_id_fk": { + "name": "environment_leases_issue_id_issues_id_fk", + "tableFrom": "environment_leases", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "environment_leases", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver": { + "name": "driver", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environments_status_idx": { + "name": "environments_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_local_driver_idx": { + "name": "environments_local_driver_idx", + "columns": [ + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environments\".\"driver\" = 'local'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_managed_sandbox_idx": { + "name": "environments_managed_sandbox_idx", + "columns": [ + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environments\".\"driver\" = 'sandbox' AND (\"environments\".\"metadata\" ->> 'managedByPaperclip')::boolean = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_name_idx": { + "name": "environments_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspace_runtime_leases": { + "name": "execution_workspace_runtime_leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_key": { + "name": "owner_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_issue_id": { + "name": "owner_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_run_id": { + "name": "owner_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_action": { + "name": "last_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "renewed_at": { + "name": "renewed_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 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspace_runtime_leases_company_workspace_idx": { + "name": "execution_workspace_runtime_leases_company_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_runtime_leases_company_owner_idx": { + "name": "execution_workspace_runtime_leases_company_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_runtime_leases_expires_at_idx": { + "name": "execution_workspace_runtime_leases_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspace_runtime_leases_company_id_companies_id_fk": { + "name": "execution_workspace_runtime_leases_company_id_companies_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspace_runtime_leases_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_owner_issue_id_issues_id_fk": { + "name": "execution_workspace_runtime_leases_owner_issue_id_issues_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "issues", + "columnsFrom": [ + "owner_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_owner_run_id_heartbeat_runs_id_fk": { + "name": "execution_workspace_runtime_leases_owner_run_id_heartbeat_runs_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "owner_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_owner_agent_id_agents_id_fk": { + "name": "execution_workspace_runtime_leases_owner_agent_id_agents_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "execution_workspace_runtime_leases_execution_workspace_id_unique": { + "name": "execution_workspace_runtime_leases_execution_workspace_id_unique", + "nullsNotDistinct": false, + "columns": [ + "execution_workspace_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspaces": { + "name": "execution_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy_type": { + "name": "strategy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_fs'" + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "derived_from_execution_workspace_id": { + "name": "derived_from_execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_eligible_at": { + "name": "cleanup_eligible_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_reason": { + "name": "cleanup_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspaces_company_project_status_idx": { + "name": "execution_workspaces_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_project_workspace_status_idx": { + "name": "execution_workspaces_company_project_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_source_issue_idx": { + "name": "execution_workspaces_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_last_used_idx": { + "name": "execution_workspaces_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_branch_idx": { + "name": "execution_workspaces_company_branch_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspaces_company_id_companies_id_fk": { + "name": "execution_workspaces_company_id_companies_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_id_projects_id_fk": { + "name": "execution_workspaces_project_id_projects_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_workspace_id_project_workspaces_id_fk": { + "name": "execution_workspaces_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_source_issue_id_issues_id_fk": { + "name": "execution_workspaces_source_issue_id_issues_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "derived_from_execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_object_mentions": { + "name": "external_object_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_record_id": { + "name": "source_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "property_key": { + "name": "property_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_text_redacted": { + "name": "matched_text_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sanitized_display_url": { + "name": "sanitized_display_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_identity_hash": { + "name": "canonical_identity_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_identity": { + "name": "canonical_identity", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detector_key": { + "name": "detector_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "object_type": { + "name": "object_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exact'" + }, + "created_by_plugin_id": { + "name": "created_by_plugin_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": { + "external_object_mentions_company_source_issue_idx": { + "name": "external_object_mentions_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_object_idx": { + "name": "external_object_mentions_company_object_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_provider_idx": { + "name": "external_object_mentions_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_source_record_uq": { + "name": "external_object_mentions_company_source_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_identity_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"external_object_mentions\".\"source_record_id\" is not null and \"external_object_mentions\".\"canonical_identity_hash\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_source_null_record_uq": { + "name": "external_object_mentions_company_source_null_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_identity_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"external_object_mentions\".\"source_record_id\" is null and \"external_object_mentions\".\"canonical_identity_hash\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_object_mentions_company_id_companies_id_fk": { + "name": "external_object_mentions_company_id_companies_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_object_mentions_source_issue_id_issues_id_fk": { + "name": "external_object_mentions_source_issue_id_issues_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_object_mentions_object_id_external_objects_id_fk": { + "name": "external_object_mentions_object_id_external_objects_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "external_objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_object_mentions_created_by_plugin_id_plugins_id_fk": { + "name": "external_object_mentions_created_by_plugin_id_plugins_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "plugins", + "columnsFrom": [ + "created_by_plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_objects": { + "name": "external_objects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "object_type": { + "name": "object_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sanitized_canonical_url": { + "name": "sanitized_canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_identity_hash": { + "name": "canonical_identity_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_key": { + "name": "display_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_key": { + "name": "icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_title": { + "name": "display_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_key": { + "name": "status_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_label": { + "name": "status_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_icon_key": { + "name": "status_icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_category": { + "name": "status_category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "status_tone": { + "name": "status_tone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'neutral'" + }, + "liveness": { + "name": "liveness", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "is_terminal": { + "name": "is_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "remote_version": { + "name": "remote_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_resolved_at": { + "name": "last_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_changed_at": { + "name": "last_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_refresh_at": { + "name": "next_refresh_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_started_at": { + "name": "refresh_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "uuid", + "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": { + "external_objects_company_provider_object_idx": { + "name": "external_objects_company_provider_object_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_provider_status_idx": { + "name": "external_objects_company_provider_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_refresh_idx": { + "name": "external_objects_company_refresh_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_refresh_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_external_id_uq": { + "name": "external_objects_company_external_id_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_identity_uq": { + "name": "external_objects_company_identity_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_identity_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_objects_company_id_companies_id_fk": { + "name": "external_objects_company_id_companies_id_fk", + "tableFrom": "external_objects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_objects_plugin_id_plugins_id_fk": { + "name": "external_objects_plugin_id_plugins_id_fk", + "tableFrom": "external_objects", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_exports": { + "name": "feedback_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "feedback_vote_id": { + "name": "feedback_vote_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "export_id": { + "name": "export_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-envelope-v2'" + }, + "bundle_version": { + "name": "bundle_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-bundle-v2'" + }, + "payload_version": { + "name": "payload_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-v1'" + }, + "payload_digest": { + "name": "payload_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_snapshot": { + "name": "payload_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_summary": { + "name": "target_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "exported_at": { + "name": "exported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_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": { + "feedback_exports_feedback_vote_idx": { + "name": "feedback_exports_feedback_vote_idx", + "columns": [ + { + "expression": "feedback_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_created_idx": { + "name": "feedback_exports_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_status_idx": { + "name": "feedback_exports_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_issue_idx": { + "name": "feedback_exports_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_project_idx": { + "name": "feedback_exports_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_author_idx": { + "name": "feedback_exports_company_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_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": { + "feedback_exports_company_id_companies_id_fk": { + "name": "feedback_exports_company_id_companies_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_exports_feedback_vote_id_feedback_votes_id_fk": { + "name": "feedback_exports_feedback_vote_id_feedback_votes_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "feedback_votes", + "columnsFrom": [ + "feedback_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_issue_id_issues_id_fk": { + "name": "feedback_exports_issue_id_issues_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_project_id_projects_id_fk": { + "name": "feedback_exports_project_id_projects_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_votes": { + "name": "feedback_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_with_labs": { + "name": "shared_with_labs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redaction_summary": { + "name": "redaction_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": { + "feedback_votes_company_issue_idx": { + "name": "feedback_votes_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_issue_target_idx": { + "name": "feedback_votes_issue_target_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_author_idx": { + "name": "feedback_votes_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_company_target_author_idx": { + "name": "feedback_votes_company_target_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_votes_company_id_companies_id_fk": { + "name": "feedback_votes_company_id_companies_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_votes_issue_id_issues_id_fk": { + "name": "feedback_votes_issue_id_issues_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folders": { + "name": "folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_key": { + "name": "system_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "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": { + "folders_company_kind_position_idx": { + "name": "folders_company_kind_position_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_root_slug_uq": { + "name": "folders_company_kind_root_slug_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folders\".\"parent_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_parent_slug_uq": { + "name": "folders_company_kind_parent_slug_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folders\".\"parent_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_system_key_uq": { + "name": "folders_company_kind_system_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "system_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folders\".\"system_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_parent_position_idx": { + "name": "folders_company_kind_parent_position_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folders_company_id_companies_id_fk": { + "name": "folders_company_id_companies_id_fk", + "tableFrom": "folders", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folders_parent_id_folders_id_fk": { + "name": "folders_parent_id_folders_id_fk", + "tableFrom": "folders", + "tableTo": "folders", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_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": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_watchdog_decisions": { + "name": "heartbeat_run_watchdog_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluation_issue_id": { + "name": "evaluation_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_watchdog_decisions_company_run_created_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_watchdog_decisions_company_run_snooze_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_snooze_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snoozed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_watchdog_decisions_company_id_companies_id_fk": { + "name": "heartbeat_run_watchdog_decisions_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk": { + "name": "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "issues", + "columnsFrom": [ + "evaluation_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_pid": { + "name": "process_pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_group_id": { + "name": "process_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_started_at": { + "name": "process_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_at": { + "name": "last_output_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_seq": { + "name": "last_output_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_output_stream": { + "name": "last_output_stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_output_bytes": { + "name": "last_output_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "retry_of_run_id": { + "name": "retry_of_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "process_loss_retry_count": { + "name": "process_loss_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_at": { + "name": "scheduled_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scheduled_retry_attempt": { + "name": "scheduled_retry_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_reason": { + "name": "scheduled_retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_comment_status": { + "name": "issue_comment_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_applicable'" + }, + "issue_comment_satisfied_by_comment_id": { + "name": "issue_comment_satisfied_by_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_comment_retry_queued_at": { + "name": "issue_comment_retry_queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "liveness_state": { + "name": "liveness_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "liveness_reason": { + "name": "liveness_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "continuation_attempt": { + "name": "continuation_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_useful_action_at": { + "name": "last_useful_action_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_snapshot": { + "name": "context_snapshot", + "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": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_responsible_user_idx": { + "name": "heartbeat_runs_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_liveness_idx": { + "name": "heartbeat_runs_company_liveness_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "liveness_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_last_output_idx": { + "name": "heartbeat_runs_company_status_last_output_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_output_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_process_started_idx": { + "name": "heartbeat_runs_company_status_process_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "process_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_created_at_desc_idx": { + "name": "heartbeat_runs_company_created_at_desc_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_ctx_issue_created_idx": { + "name": "heartbeat_runs_company_ctx_issue_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"context_snapshot\" ->> 'issueId')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_ctx_task_created_idx": { + "name": "heartbeat_runs_company_ctx_task_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"context_snapshot\" ->> 'taskId')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_ctx_taskkey_created_idx": { + "name": "heartbeat_runs_company_ctx_taskkey_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"context_snapshot\" ->> 'taskKey')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "retry_of_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_dismissals": { + "name": "inbox_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "name": "item_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dismiss'" + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "snoozed_until": { + "name": "snoozed_until", + "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": { + "inbox_dismissals_company_user_idx": { + "name": "inbox_dismissals_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_item_idx": { + "name": "inbox_dismissals_company_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_user_item_idx": { + "name": "inbox_dismissals_company_user_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_dismissals_company_id_companies_id_fk": { + "name": "inbox_dismissals_company_id_companies_id_fk", + "tableFrom": "inbox_dismissals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_grants": { + "name": "connection_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant": { + "name": "provider_tenant", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_secret_refs": { + "name": "credential_secret_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_by_agent_id": { + "name": "revoked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revoked_by_user_id": { + "name": "revoked_by_user_id", + "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": { + "connection_grants_company_connection_idx": { + "name": "connection_grants_company_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_grants_subject_user_idx": { + "name": "connection_grants_subject_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_grants_user_uq": { + "name": "connection_grants_user_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_grants_default_uq": { + "name": "connection_grants_default_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_grants\".\"is_default\" = true and \"connection_grants\".\"kind\" = 'workspace'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connection_grants_company_id_companies_id_fk": { + "name": "connection_grants_company_id_companies_id_fk", + "tableFrom": "connection_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_grants_created_by_agent_id_agents_id_fk": { + "name": "connection_grants_created_by_agent_id_agents_id_fk", + "tableFrom": "connection_grants", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_grants_revoked_by_agent_id_agents_id_fk": { + "name": "connection_grants_revoked_by_agent_id_agents_id_fk", + "tableFrom": "connection_grants", + "tableTo": "agents", + "columnsFrom": [ + "revoked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_grants_company_connection_fk": { + "name": "connection_grants_company_connection_fk", + "tableFrom": "connection_grants", + "tableTo": "tool_connections", + "columnsFrom": [ + "company_id", + "connection_id" + ], + "columnsTo": [ + "company_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "connection_grants_kind_check": { + "name": "connection_grants_kind_check", + "value": "\"connection_grants\".\"kind\" in ('workspace', 'user')" + }, + "connection_grants_status_check": { + "name": "connection_grants_status_check", + "value": "\"connection_grants\".\"status\" in ('active', 'revoked', 'expired', 'needs_reauthorization')" + }, + "connection_grants_subject_check": { + "name": "connection_grants_subject_check", + "value": "(\"connection_grants\".\"kind\" = 'user' and \"connection_grants\".\"subject_user_id\" is not null) or (\"connection_grants\".\"kind\" = 'workspace' and \"connection_grants\".\"subject_user_id\" is null)" + }, + "connection_grants_default_check": { + "name": "connection_grants_default_check", + "value": "\"connection_grants\".\"is_default\" = false or \"connection_grants\".\"kind\" = 'workspace'" + } + }, + "isRLSEnabled": false + }, + "public.connection_token_issuances": { + "name": "connection_token_issuances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_scope": { + "name": "requested_scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "issued_scope": { + "name": "issued_scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_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()" + } + }, + "indexes": { + "connection_token_issuances_company_created_idx": { + "name": "connection_token_issuances_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_token_issuances_connection_created_idx": { + "name": "connection_token_issuances_connection_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_token_issuances_agent_connection_idx": { + "name": "connection_token_issuances_agent_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_token_issuances_run_idx": { + "name": "connection_token_issuances_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connection_token_issuances_company_id_companies_id_fk": { + "name": "connection_token_issuances_company_id_companies_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_token_issuances_application_id_tool_applications_id_fk": { + "name": "connection_token_issuances_application_id_tool_applications_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_token_issuances_connection_id_tool_connections_id_fk": { + "name": "connection_token_issuances_connection_id_tool_connections_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_token_issuances_agent_id_agents_id_fk": { + "name": "connection_token_issuances_agent_id_agents_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_token_issuances_run_id_heartbeat_runs_id_fk": { + "name": "connection_token_issuances_run_id_heartbeat_runs_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_token_issuances_issue_id_issues_id_fk": { + "name": "connection_token_issuances_issue_id_issues_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_token_issuances_project_id_projects_id_fk": { + "name": "connection_token_issuances_project_id_projects_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "singleton_key": { + "name": "singleton_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "experimental": { + "name": "experimental", + "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": { + "instance_settings_singleton_key_idx": { + "name": "instance_settings_singleton_key_idx", + "columns": [ + { + "expression": "singleton_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "instance_settings_default_environment_id_environments_id_fk": { + "name": "instance_settings_default_environment_id_environments_id_fk", + "tableFrom": "instance_settings", + "tableTo": "environments", + "columnsFrom": [ + "default_environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "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 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_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": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_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": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_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": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "on_behalf_of_user_id": { + "name": "on_behalf_of_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_type": { + "name": "author_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "derived_author_agent_id": { + "name": "derived_author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "derived_created_by_run_id": { + "name": "derived_created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "derived_author_source": { + "name": "derived_author_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "presentation": { + "name": "presentation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_by_type": { + "name": "deleted_by_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_by_agent_id": { + "name": "deleted_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deleted_by_user_id": { + "name": "deleted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_by_run_id": { + "name": "deleted_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "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": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_body_search_idx": { + "name": "issue_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_on_behalf_of_user_id_user_id_fk": { + "name": "issue_comments_on_behalf_of_user_id_user_id_fk", + "tableFrom": "issue_comments", + "tableTo": "user", + "columnsFrom": [ + "on_behalf_of_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_derived_author_agent_id_agents_id_fk": { + "name": "issue_comments_derived_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "derived_author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_derived_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_derived_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "derived_created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_deleted_by_agent_id_agents_id_fk": { + "name": "issue_comments_deleted_by_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "deleted_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_deleted_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_deleted_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "deleted_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_create_idempotency_keys": { + "name": "issue_create_idempotency_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_create_idempotency_keys_company_key_uq": { + "name": "issue_create_idempotency_keys_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_create_idempotency_keys_issue_idx": { + "name": "issue_create_idempotency_keys_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_create_idempotency_keys_company_created_at_idx": { + "name": "issue_create_idempotency_keys_company_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_create_idempotency_keys_company_id_companies_id_fk": { + "name": "issue_create_idempotency_keys_company_id_companies_id_fk", + "tableFrom": "issue_create_idempotency_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_create_idempotency_keys_issue_id_issues_id_fk": { + "name": "issue_create_idempotency_keys_issue_id_issues_id_fk", + "tableFrom": "issue_create_idempotency_keys", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_execution_decisions": { + "name": "issue_execution_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_type": { + "name": "stage_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_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": { + "issue_execution_decisions_company_issue_idx": { + "name": "issue_execution_decisions_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_execution_decisions_stage_idx": { + "name": "issue_execution_decisions_stage_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_execution_decisions_company_id_companies_id_fk": { + "name": "issue_execution_decisions_company_id_companies_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_issue_id_issues_id_fk": { + "name": "issue_execution_decisions_issue_id_issues_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_execution_decisions_actor_agent_id_agents_id_fk": { + "name": "issue_execution_decisions_actor_agent_id_agents_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_inbox_archives": { + "name": "issue_inbox_archives", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_by_actor_type": { + "name": "archived_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "archived_by_agent_id": { + "name": "archived_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_by_run_id": { + "name": "archived_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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": { + "issue_inbox_archives_company_issue_idx": { + "name": "issue_inbox_archives_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_user_idx": { + "name": "issue_inbox_archives_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_issue_user_idx": { + "name": "issue_inbox_archives_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_inbox_archives_company_id_companies_id_fk": { + "name": "issue_inbox_archives_company_id_companies_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_inbox_archives_issue_id_issues_id_fk": { + "name": "issue_inbox_archives_issue_id_issues_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_inbox_archives_archived_by_agent_id_agents_id_fk": { + "name": "issue_inbox_archives_archived_by_agent_id_agents_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "agents", + "columnsFrom": [ + "archived_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_inbox_archives_archived_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_inbox_archives_archived_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "archived_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "issue_inbox_archives_archived_by_actor_type_check": { + "name": "issue_inbox_archives_archived_by_actor_type_check", + "value": "\"issue_inbox_archives\".\"archived_by_actor_type\" in ('user', 'agent')" + } + }, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_plan_decompositions": { + "name": "issue_plan_decompositions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accepted_plan_revision_id": { + "name": "accepted_plan_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accepted_interaction_id": { + "name": "accepted_interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'in_flight'" + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_child_count": { + "name": "requested_child_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_children": { + "name": "requested_children", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "child_issue_ids": { + "name": "child_issue_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_run_id": { + "name": "owner_run_id", + "type": "uuid", + "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": { + "issue_plan_decompositions_company_source_status_idx": { + "name": "issue_plan_decompositions_company_source_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_plan_decompositions_active_owner_idx": { + "name": "issue_plan_decompositions_active_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"issue_plan_decompositions\".\"status\" = 'in_flight'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_plan_decompositions_source_revision_uq": { + "name": "issue_plan_decompositions_source_revision_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted_plan_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_plan_decompositions_company_id_companies_id_fk": { + "name": "issue_plan_decompositions_company_id_companies_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_plan_decompositions_source_issue_id_issues_id_fk": { + "name": "issue_plan_decompositions_source_issue_id_issues_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_plan_decompositions_accepted_plan_revision_id_document_revisions_id_fk": { + "name": "issue_plan_decompositions_accepted_plan_revision_id_document_revisions_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "document_revisions", + "columnsFrom": [ + "accepted_plan_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_plan_decompositions_accepted_interaction_id_issue_thread_interactions_id_fk": { + "name": "issue_plan_decompositions_accepted_interaction_id_issue_thread_interactions_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "issue_thread_interactions", + "columnsFrom": [ + "accepted_interaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_plan_decompositions_owner_agent_id_agents_id_fk": { + "name": "issue_plan_decompositions_owner_agent_id_agents_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_plan_decompositions_owner_run_id_heartbeat_runs_id_fk": { + "name": "issue_plan_decompositions_owner_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "owner_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_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": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_recovery_actions": { + "name": "issue_recovery_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recovery_issue_id": { + "name": "recovery_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_owner_agent_id": { + "name": "previous_owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "return_owner_agent_id": { + "name": "return_owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cause": { + "name": "cause", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wake_policy": { + "name": "wake_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "monitor_policy": { + "name": "monitor_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timeout_at": { + "name": "timeout_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 + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "issue_recovery_actions_company_source_status_idx": { + "name": "issue_recovery_actions_company_source_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_company_owner_status_idx": { + "name": "issue_recovery_actions_company_owner_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_company_recovery_issue_idx": { + "name": "issue_recovery_actions_company_recovery_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recovery_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_active_source_uq": { + "name": "issue_recovery_actions_active_source_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_recovery_actions\".\"status\" in ('active', 'escalated')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_active_fingerprint_uq": { + "name": "issue_recovery_actions_active_fingerprint_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cause", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_recovery_actions\".\"status\" in ('active', 'escalated')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_recovery_actions_company_id_companies_id_fk": { + "name": "issue_recovery_actions_company_id_companies_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_recovery_actions_source_issue_id_issues_id_fk": { + "name": "issue_recovery_actions_source_issue_id_issues_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_recovery_actions_recovery_issue_id_issues_id_fk": { + "name": "issue_recovery_actions_recovery_issue_id_issues_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "issues", + "columnsFrom": [ + "recovery_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_recovery_actions_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_recovery_actions_previous_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_previous_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "agents", + "columnsFrom": [ + "previous_owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_recovery_actions_return_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_return_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "agents", + "columnsFrom": [ + "return_owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_reference_mentions": { + "name": "issue_reference_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_issue_id": { + "name": "target_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_record_id": { + "name": "source_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_text": { + "name": "matched_text", + "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": { + "issue_reference_mentions_company_source_issue_idx": { + "name": "issue_reference_mentions_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_target_issue_idx": { + "name": "issue_reference_mentions_company_target_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_issue_pair_idx": { + "name": "issue_reference_mentions_company_issue_pair_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_record_uq": { + "name": "issue_reference_mentions_company_source_mention_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_null_record_uq": { + "name": "issue_reference_mentions_company_source_mention_null_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_reference_mentions_company_id_companies_id_fk": { + "name": "issue_reference_mentions_company_id_companies_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_reference_mentions_source_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_source_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_reference_mentions_target_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_target_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "target_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_relations": { + "name": "issue_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "related_issue_id": { + "name": "related_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_relations_company_issue_idx": { + "name": "issue_relations_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_related_issue_idx": { + "name": "issue_relations_company_related_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_type_idx": { + "name": "issue_relations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_edge_uq": { + "name": "issue_relations_company_edge_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_relations_company_id_companies_id_fk": { + "name": "issue_relations_company_id_companies_id_fk", + "tableFrom": "issue_relations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_relations_issue_id_issues_id_fk": { + "name": "issue_relations_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_related_issue_id_issues_id_fk": { + "name": "issue_relations_related_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "related_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_created_by_agent_id_agents_id_fk": { + "name": "issue_relations_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_relations", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_thread_interactions": { + "name": "issue_thread_interactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "continuation_policy": { + "name": "continuation_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'wake_assignee'" + }, + "requested_resolver_policy": { + "name": "requested_resolver_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'anyone'" + }, + "effective_resolver_policy": { + "name": "effective_resolver_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'anyone'" + }, + "resolver_policy_provenance": { + "name": "resolver_policy_provenance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherited'" + }, + "effective_resolver_policy_source": { + "name": "effective_resolver_policy_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_comment_id": { + "name": "source_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "addressee_agent_id": { + "name": "addressee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_run_id": { + "name": "resolved_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "issue_thread_interactions_issue_idx": { + "name": "issue_thread_interactions_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_created_at_idx": { + "name": "issue_thread_interactions_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_status_idx": { + "name": "issue_thread_interactions_company_issue_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_idempotency_uq": { + "name": "issue_thread_interactions_company_issue_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_thread_interactions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_source_comment_idx": { + "name": "issue_thread_interactions_source_comment_idx", + "columns": [ + { + "expression": "source_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_addressee_agent_idx": { + "name": "issue_thread_interactions_addressee_agent_idx", + "columns": [ + { + "expression": "addressee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_thread_interactions_company_id_companies_id_fk": { + "name": "issue_thread_interactions_company_id_companies_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_issue_id_issues_id_fk": { + "name": "issue_thread_interactions_issue_id_issues_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_comment_id_issue_comments_id_fk": { + "name": "issue_thread_interactions_source_comment_id_issue_comments_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issue_comments", + "columnsFrom": [ + "source_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk": { + "name": "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_created_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_addressee_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_addressee_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "addressee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_resolved_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_resolved_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_resolved_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_thread_interactions_resolved_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "resolved_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_hold_members": { + "name": "issue_tree_hold_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "issue_identifier": { + "name": "issue_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_status": { + "name": "issue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_run_id": { + "name": "active_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_run_status": { + "name": "active_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skipped": { + "name": "skipped", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_hold_members_hold_issue_uq": { + "name": "issue_tree_hold_members_hold_issue_uq", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_company_issue_idx": { + "name": "issue_tree_hold_members_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_hold_depth_idx": { + "name": "issue_tree_hold_members_hold_depth_idx", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "depth", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_hold_members_company_id_companies_id_fk": { + "name": "issue_tree_hold_members_company_id_companies_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk": { + "name": "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issue_tree_holds", + "columnsFrom": [ + "hold_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_parent_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_parent_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_assignee_agent_id_agents_id_fk": { + "name": "issue_tree_hold_members_assignee_agent_id_agents_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "active_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_holds": { + "name": "issue_tree_holds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_issue_id": { + "name": "root_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_policy": { + "name": "release_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_by_actor_type": { + "name": "released_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_agent_id": { + "name": "released_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_by_user_id": { + "name": "released_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_run_id": { + "name": "released_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_metadata": { + "name": "release_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_holds_company_root_status_idx": { + "name": "issue_tree_holds_company_root_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_holds_company_status_mode_idx": { + "name": "issue_tree_holds_company_status_mode_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_holds_company_id_companies_id_fk": { + "name": "issue_tree_holds_company_id_companies_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_holds_root_issue_id_issues_id_fk": { + "name": "issue_tree_holds_root_issue_id_issues_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "issues", + "columnsFrom": [ + "root_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_released_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "released_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "released_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_watchdogs": { + "name": "issue_watchdogs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watchdog_agent_id": { + "name": "watchdog_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "watchdog_issue_id": { + "name": "watchdog_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_observed_fingerprint": { + "name": "last_observed_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_reviewed_fingerprint": { + "name": "last_reviewed_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_observed_stop_snapshot": { + "name": "last_observed_stop_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_reviewed_stop_snapshot": { + "name": "last_reviewed_stop_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_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 + }, + "trigger_count": { + "name": "trigger_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_run_id": { + "name": "updated_by_run_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": { + "issue_watchdogs_company_issue_uq": { + "name": "issue_watchdogs_company_issue_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_watchdogs_company_status_idx": { + "name": "issue_watchdogs_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_watchdogs_company_agent_idx": { + "name": "issue_watchdogs_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "watchdog_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_watchdogs_company_watchdog_issue_uq": { + "name": "issue_watchdogs_company_watchdog_issue_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "watchdog_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_watchdogs\".\"watchdog_issue_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_watchdogs_company_id_companies_id_fk": { + "name": "issue_watchdogs_company_id_companies_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_watchdogs_issue_id_issues_id_fk": { + "name": "issue_watchdogs_issue_id_issues_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_watchdogs_watchdog_agent_id_agents_id_fk": { + "name": "issue_watchdogs_watchdog_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "agents", + "columnsFrom": [ + "watchdog_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_watchdogs_watchdog_issue_id_issues_id_fk": { + "name": "issue_watchdogs_watchdog_issue_id_issues_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "issues", + "columnsFrom": [ + "watchdog_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_created_by_agent_id_agents_id_fk": { + "name": "issue_watchdogs_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_watchdogs_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_updated_by_agent_id_agents_id_fk": { + "name": "issue_watchdogs_updated_by_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_updated_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_watchdogs_updated_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "updated_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_work_products": { + "name": "issue_work_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_service_id": { + "name": "runtime_service_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_state": { + "name": "review_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_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": { + "issue_work_products_company_issue_type_idx": { + "name": "issue_work_products_company_issue_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_execution_workspace_type_idx": { + "name": "issue_work_products_company_execution_workspace_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_provider_external_id_idx": { + "name": "issue_work_products_company_provider_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_updated_idx": { + "name": "issue_work_products_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_work_products_company_id_companies_id_fk": { + "name": "issue_work_products_company_id_companies_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_work_products_project_id_projects_id_fk": { + "name": "issue_work_products_project_id_projects_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_issue_id_issues_id_fk": { + "name": "issue_work_products_issue_id_issues_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_work_products_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issue_work_products_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk": { + "name": "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "workspace_runtime_services", + "columnsFrom": [ + "runtime_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_work_products_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "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": "'backlog'" + }, + "work_mode": { + "name": "work_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "harness_kind": { + "name": "harness_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "review_policy": { + "name": "review_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_fingerprint": { + "name": "origin_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_policy": { + "name": "execution_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_state": { + "name": "execution_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "monitor_next_check_at": { + "name": "monitor_next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_wake_requested_at": { + "name": "monitor_wake_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_last_triggered_at": { + "name": "monitor_last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_attempt_count": { + "name": "monitor_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "monitor_notes": { + "name": "monitor_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monitor_scheduled_by": { + "name": "monitor_scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_preference": { + "name": "execution_workspace_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "unblock_descriptor": { + "name": "unblock_descriptor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "blocked_transition_at": { + "name": "blocked_transition_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_owner_notified_at": { + "name": "blocked_owner_notified_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 + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_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": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_harness_kind_idx": { + "name": "issues_company_harness_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "harness_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_responsible_user_idx": { + "name": "issues_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_origin_idx": { + "name": "issues_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_workspace_idx": { + "name": "issues_company_project_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_execution_workspace_idx": { + "name": "issues_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_monitor_due_idx": { + "name": "issues_company_monitor_due_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "monitor_next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_updated_idx": { + "name": "issues_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_created_idx": { + "name": "issues_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_open_normalized_title_created_idx": { + "name": "issues_open_normalized_title_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(regexp_replace(btrim(\"title\"), '\\s+', ' ', 'g'))", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"issues\".\"hidden_at\" is null and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_priority_idx": { + "name": "issues_company_priority_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_title_search_idx": { + "name": "issues_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_identifier_search_idx": { + "name": "issues_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_description_search_idx": { + "name": "issues_description_search_idx", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_open_routine_execution_uq": { + "name": "issues_open_routine_execution_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'routine_execution'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"execution_run_id\" is not null\n and \"issues\".\"status\" in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_incident_uq": { + "name": "issues_active_liveness_recovery_incident_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_leaf_uq": { + "name": "issues_active_liveness_recovery_leaf_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_fingerprint\" <> 'default'\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stale_run_evaluation_uq": { + "name": "issues_active_stale_run_evaluation_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stale_active_run_evaluation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_task_watchdog_uq": { + "name": "issues_active_task_watchdog_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'task_watchdog'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_productivity_review_uq": { + "name": "issues_active_productivity_review_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'issue_productivity_review'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stranded_issue_recovery_uq": { + "name": "issues_active_stranded_issue_recovery_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stranded_issue_recovery'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_onboarding_first_task_uq": { + "name": "issues_onboarding_first_task_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'onboarding_first_task'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_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": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_user_uq": { + "name": "join_requests_pending_human_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requesting_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"requesting_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_email_uq": { + "name": "join_requests_pending_human_email_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"request_email_snapshot\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"request_email_snapshot\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "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": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_automation_executions": { + "name": "pipeline_automation_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggering_event_id": { + "name": "triggering_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_issue_id": { + "name": "execution_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retry_of_execution_id": { + "name": "retry_of_execution_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_automation_executions_idempotency_uq": { + "name": "pipeline_automation_executions_idempotency_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "triggering_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_company_case_idx": { + "name": "pipeline_automation_executions_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_routine_idx": { + "name": "pipeline_automation_executions_routine_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_execution_issue_idx": { + "name": "pipeline_automation_executions_execution_issue_idx", + "columns": [ + { + "expression": "execution_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_retry_of_execution_idx": { + "name": "pipeline_automation_executions_retry_of_execution_idx", + "columns": [ + { + "expression": "retry_of_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_automation_executions_company_id_companies_id_fk": { + "name": "pipeline_automation_executions_company_id_companies_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_automation_executions_case_id_pipeline_cases_id_fk": { + "name": "pipeline_automation_executions_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_automation_executions_routine_id_routines_id_fk": { + "name": "pipeline_automation_executions_routine_id_routines_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_automation_executions_execution_issue_id_issues_id_fk": { + "name": "pipeline_automation_executions_execution_issue_id_issues_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "issues", + "columnsFrom": [ + "execution_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_automation_executions_status_check": { + "name": "pipeline_automation_executions_status_check", + "value": "\"pipeline_automation_executions\".\"status\" in ('succeeded', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_case_blockers": { + "name": "pipeline_case_blockers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blocked_by_case_id": { + "name": "blocked_by_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_case_blockers_case_blocked_by_uq": { + "name": "pipeline_case_blockers_case_blocked_by_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blocked_by_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_blockers_blocked_by_idx": { + "name": "pipeline_case_blockers_blocked_by_idx", + "columns": [ + { + "expression": "blocked_by_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_blockers_company_case_idx": { + "name": "pipeline_case_blockers_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_blockers_company_id_companies_id_fk": { + "name": "pipeline_case_blockers_company_id_companies_id_fk", + "tableFrom": "pipeline_case_blockers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_blockers_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_blockers_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_blockers", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_blockers_blocked_by_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_blockers_blocked_by_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_blockers", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "blocked_by_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_case_blockers_no_self_block_check": { + "name": "pipeline_case_blockers_no_self_block_check", + "value": "\"pipeline_case_blockers\".\"case_id\" <> \"pipeline_case_blockers\".\"blocked_by_case_id\"" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_case_documents": { + "name": "pipeline_case_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "pipeline_case_documents_company_case_key_uq": { + "name": "pipeline_case_documents_company_case_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_documents_document_uq": { + "name": "pipeline_case_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_documents_company_case_updated_idx": { + "name": "pipeline_case_documents_company_case_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_documents_company_id_companies_id_fk": { + "name": "pipeline_case_documents_company_id_companies_id_fk", + "tableFrom": "pipeline_case_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_documents_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_documents_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_documents", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_documents_document_id_documents_id_fk": { + "name": "pipeline_case_documents_document_id_documents_id_fk", + "tableFrom": "pipeline_case_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_case_events": { + "name": "pipeline_case_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "from_stage_id": { + "name": "from_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_stage_id": { + "name": "to_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_case_events_case_created_idx": { + "name": "pipeline_case_events_case_created_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_events_company_case_idx": { + "name": "pipeline_case_events_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_events_company_id_companies_id_fk": { + "name": "pipeline_case_events_company_id_companies_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_events_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_events_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_events_actor_agent_id_agents_id_fk": { + "name": "pipeline_case_events_actor_agent_id_agents_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_case_events_from_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_case_events_from_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "from_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_case_events_to_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_case_events_to_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "to_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_case_events_type_check": { + "name": "pipeline_case_events_type_check", + "value": "\"pipeline_case_events\".\"type\" in (\n 'ingested',\n 'updated',\n 'claimed',\n 'lease_released',\n 'lease_expired',\n 'transitioned',\n 'transition_forced',\n 'transition_suggested',\n 'suggestion_resolved',\n 'review_decided',\n 'conversation_opened',\n 'issue_linked',\n 'issue_unlinked',\n 'automation_executed',\n 'automation_failed',\n 'automation_retry_requested',\n 'automation_effects_retired',\n 'automation_retry_dispatched',\n 'blockers_set',\n 'blockers_resolved',\n 'children_terminal',\n 'upstream_drift',\n 'drift_acknowledged'\n )" + }, + "pipeline_case_events_actor_type_check": { + "name": "pipeline_case_events_actor_type_check", + "value": "\"pipeline_case_events\".\"actor_type\" in ('user', 'agent', 'system')" + }, + "pipeline_case_events_agent_run_check": { + "name": "pipeline_case_events_agent_run_check", + "value": "\"pipeline_case_events\".\"actor_type\" <> 'agent' or \"pipeline_case_events\".\"run_id\" is not null" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_case_issue_links": { + "name": "pipeline_case_issue_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "automation_attempt_id": { + "name": "automation_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_by_attempt_id": { + "name": "retired_by_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retired_reason": { + "name": "retired_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": { + "pipeline_case_issue_links_case_issue_uq": { + "name": "pipeline_case_issue_links_case_issue_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_issue_links_issue_idx": { + "name": "pipeline_case_issue_links_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_issue_links_company_case_idx": { + "name": "pipeline_case_issue_links_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_issue_links_automation_attempt_idx": { + "name": "pipeline_case_issue_links_automation_attempt_idx", + "columns": [ + { + "expression": "automation_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_issue_links_company_id_companies_id_fk": { + "name": "pipeline_case_issue_links_company_id_companies_id_fk", + "tableFrom": "pipeline_case_issue_links", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_issue_links_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_issue_links_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_issue_links", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_issue_links_issue_id_issues_id_fk": { + "name": "pipeline_case_issue_links_issue_id_issues_id_fk", + "tableFrom": "pipeline_case_issue_links", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_case_issue_links_role_check": { + "name": "pipeline_case_issue_links_role_check", + "value": "\"pipeline_case_issue_links\".\"role\" in ('origin', 'conversation', 'work', 'automation')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_cases": { + "name": "pipeline_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_key": { + "name": "case_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "workspace_ref": { + "name": "workspace_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parent_case_id": { + "name": "parent_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_case_version": { + "name": "parent_case_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_key": { + "name": "request_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_attempt_id": { + "name": "automation_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "pending_suggestion": { + "name": "pending_suggestion", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lease_owner_type": { + "name": "lease_owner_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_agent_id": { + "name": "lease_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_user_id": { + "name": "lease_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "terminal_kind": { + "name": "terminal_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_by_attempt_id": { + "name": "retired_by_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retired_reason": { + "name": "retired_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hidden_from_board_at": { + "name": "hidden_from_board_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "child_count": { + "name": "child_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "terminal_child_count": { + "name": "terminal_child_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_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": { + "pipeline_cases_pipeline_case_key_uq": { + "name": "pipeline_cases_pipeline_case_key_uq", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_parent_request_key_uq": { + "name": "pipeline_cases_parent_request_key_uq", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"pipeline_cases\".\"request_key\" is not null and \"pipeline_cases\".\"retired_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_company_idx": { + "name": "pipeline_cases_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_pipeline_stage_idx": { + "name": "pipeline_cases_pipeline_stage_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_parent_idx": { + "name": "pipeline_cases_parent_idx", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_automation_attempt_idx": { + "name": "pipeline_cases_automation_attempt_idx", + "columns": [ + { + "expression": "automation_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_retired_idx": { + "name": "pipeline_cases_retired_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retired_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_lease_expires_idx": { + "name": "pipeline_cases_lease_expires_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pipeline_cases\".\"lease_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_cases_company_id_companies_id_fk": { + "name": "pipeline_cases_company_id_companies_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_cases_pipeline_id_pipelines_id_fk": { + "name": "pipeline_cases_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_cases_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_cases_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pipeline_cases_parent_case_id_pipeline_cases_id_fk": { + "name": "pipeline_cases_parent_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "parent_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_cases_lease_agent_id_agents_id_fk": { + "name": "pipeline_cases_lease_agent_id_agents_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "agents", + "columnsFrom": [ + "lease_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_cases_created_by_agent_id_agents_id_fk": { + "name": "pipeline_cases_created_by_agent_id_agents_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_cases_terminal_kind_check": { + "name": "pipeline_cases_terminal_kind_check", + "value": "\"pipeline_cases\".\"terminal_kind\" is null or \"pipeline_cases\".\"terminal_kind\" in ('done', 'cancelled')" + }, + "pipeline_cases_lease_owner_type_check": { + "name": "pipeline_cases_lease_owner_type_check", + "value": "\"pipeline_cases\".\"lease_owner_type\" is null or \"pipeline_cases\".\"lease_owner_type\" in ('user', 'agent')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_documents": { + "name": "pipeline_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "pipeline_documents_company_pipeline_key_uq": { + "name": "pipeline_documents_company_pipeline_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_documents_document_uq": { + "name": "pipeline_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_documents_company_pipeline_updated_idx": { + "name": "pipeline_documents_company_pipeline_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_documents_company_id_companies_id_fk": { + "name": "pipeline_documents_company_id_companies_id_fk", + "tableFrom": "pipeline_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_documents_pipeline_id_pipelines_id_fk": { + "name": "pipeline_documents_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_documents", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_documents_document_id_documents_id_fk": { + "name": "pipeline_documents_document_id_documents_id_fk", + "tableFrom": "pipeline_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_stages": { + "name": "pipeline_stages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "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": { + "pipeline_stages_pipeline_key_uq": { + "name": "pipeline_stages_pipeline_key_uq", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_stages_pipeline_position_idx": { + "name": "pipeline_stages_pipeline_position_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_stages_pipeline_id_pipelines_id_fk": { + "name": "pipeline_stages_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_stages", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_stages_kind_check": { + "name": "pipeline_stages_kind_check", + "value": "\"pipeline_stages\".\"kind\" in ('working', 'review', 'done', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_transitions": { + "name": "pipeline_transitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_stage_id": { + "name": "from_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_stage_id": { + "name": "to_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "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": { + "pipeline_transitions_pipeline_edge_uq": { + "name": "pipeline_transitions_pipeline_edge_uq", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_transitions_pipeline_from_idx": { + "name": "pipeline_transitions_pipeline_from_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_transitions_pipeline_to_idx": { + "name": "pipeline_transitions_pipeline_to_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_transitions_pipeline_id_pipelines_id_fk": { + "name": "pipeline_transitions_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_transitions", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_transitions_from_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_transitions_from_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_transitions", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "from_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_transitions_to_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_transitions_to_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_transitions", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "to_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipelines": { + "name": "pipelines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enforce_transitions": { + "name": "enforce_transitions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipelines_company_key_uq": { + "name": "pipelines_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipelines_company_idx": { + "name": "pipelines_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipelines_company_project_idx": { + "name": "pipelines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipelines_company_id_companies_id_fk": { + "name": "pipelines_company_id_companies_id_fk", + "tableFrom": "pipelines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipelines_project_id_projects_id_fk": { + "name": "pipelines_project_id_projects_id_fk", + "tableFrom": "pipelines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipelines_created_by_agent_id_agents_id_fk": { + "name": "pipelines_created_by_agent_id_agents_id_fk", + "tableFrom": "pipelines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_company_idx": { + "name": "plugin_config_plugin_company_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_config_company_id_companies_id_fk": { + "name": "plugin_config_company_id_companies_id_fk", + "tableFrom": "plugin_config", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_database_namespaces": { + "name": "plugin_database_namespaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_mode": { + "name": "namespace_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'schema'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_database_namespaces_plugin_idx": { + "name": "plugin_database_namespaces_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_namespace_idx": { + "name": "plugin_database_namespaces_namespace_idx", + "columns": [ + { + "expression": "namespace_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_status_idx": { + "name": "plugin_database_namespaces_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_database_namespaces_plugin_id_plugins_id_fk": { + "name": "plugin_database_namespaces_plugin_id_plugins_id_fk", + "tableFrom": "plugin_database_namespaces", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "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": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_company_idx": { + "name": "plugin_entities_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_entities_company_id_companies_id_fk": { + "name": "plugin_entities_company_id_companies_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "nullsNotDistinct": true, + "columns": [ + "company_id", + "plugin_id", + "entity_type", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_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": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_company_idx": { + "name": "plugin_job_runs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_company_id_companies_id_fk": { + "name": "plugin_job_runs_company_id_companies_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_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": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_company_idx": { + "name": "plugin_logs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_logs_company_id_companies_id_fk": { + "name": "plugin_logs_company_id_companies_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_managed_resources": { + "name": "plugin_managed_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_kind": { + "name": "resource_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "defaults_json": { + "name": "defaults_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": { + "plugin_managed_resources_company_idx": { + "name": "plugin_managed_resources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_plugin_idx": { + "name": "plugin_managed_resources_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_resource_idx": { + "name": "plugin_managed_resources_resource_idx", + "columns": [ + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_company_plugin_resource_uq": { + "name": "plugin_managed_resources_company_plugin_resource_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_managed_resources_company_id_companies_id_fk": { + "name": "plugin_managed_resources_company_id_companies_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_managed_resources_plugin_id_plugins_id_fk": { + "name": "plugin_managed_resources_plugin_id_plugins_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_key": { + "name": "migration_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "plugin_migrations_plugin_key_idx": { + "name": "plugin_migrations_plugin_key_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_plugin_idx": { + "name": "plugin_migrations_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_status_idx": { + "name": "plugin_migrations_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_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": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_company_idx": { + "name": "plugin_webhook_deliveries_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_webhook_deliveries_company_id_companies_id_fk": { + "name": "plugin_webhook_deliveries_company_id_companies_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_memberships": { + "name": "project_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'joined'" + }, + "starred_at": { + "name": "starred_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": { + "project_memberships_company_user_idx": { + "name": "project_memberships_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_memberships_company_user_starred_idx": { + "name": "project_memberships_company_user_starred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_memberships_project_idx": { + "name": "project_memberships_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_memberships_company_user_project_uq": { + "name": "project_memberships_company_user_project_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_memberships_company_id_companies_id_fk": { + "name": "project_memberships_company_id_companies_id_fk", + "tableFrom": "project_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_memberships_project_id_projects_id_fk": { + "name": "project_memberships_project_id_projects_id_fk", + "tableFrom": "project_memberships", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ref": { + "name": "default_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "setup_command": { + "name": "setup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_command": { + "name": "cleanup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_provider": { + "name": "remote_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_workspace_ref": { + "name": "remote_workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_workspace_key": { + "name": "shared_workspace_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "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": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_source_type_idx": { + "name": "project_workspaces_project_source_type_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_company_shared_key_idx": { + "name": "project_workspaces_company_shared_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_workspace_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_remote_ref_idx": { + "name": "project_workspaces_project_remote_ref_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_workspace_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_documents": { + "name": "routine_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "routine_documents_company_routine_key_uq": { + "name": "routine_documents_company_routine_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_documents_document_uq": { + "name": "routine_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_documents_company_routine_updated_idx": { + "name": "routine_documents_company_routine_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_documents_company_id_companies_id_fk": { + "name": "routine_documents_company_id_companies_id_fk", + "tableFrom": "routine_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routine_documents_routine_id_routines_id_fk": { + "name": "routine_documents_routine_id_routines_id_fk", + "tableFrom": "routine_documents", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_documents_document_id_documents_id_fk": { + "name": "routine_documents_document_id_documents_id_fk", + "tableFrom": "routine_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_revisions": { + "name": "routine_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "restored_from_revision_id": { + "name": "restored_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_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": { + "routine_revisions_routine_revision_uq": { + "name": "routine_revisions_routine_revision_uq", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_revisions_company_routine_created_idx": { + "name": "routine_revisions_company_routine_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_revisions_company_responsible_user_idx": { + "name": "routine_revisions_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_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": { + "routine_revisions_company_id_companies_id_fk": { + "name": "routine_revisions_company_id_companies_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_revisions_routine_id_routines_id_fk": { + "name": "routine_revisions_routine_id_routines_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_revisions_restored_from_revision_id_routine_revisions_id_fk": { + "name": "routine_revisions_restored_from_revision_id_routine_revisions_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "routine_revisions", + "columnsFrom": [ + "restored_from_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_revisions_created_by_agent_id_agents_id_fk": { + "name": "routine_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "routine_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "routine_revision_id": { + "name": "routine_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_payload": { + "name": "trigger_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dispatch_fingerprint": { + "name": "dispatch_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_issue_id": { + "name": "linked_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "coalesced_into_run_id": { + "name": "coalesced_into_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "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": { + "routine_runs_company_routine_idx": { + "name": "routine_runs_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_revision_idx": { + "name": "routine_runs_revision_idx", + "columns": [ + { + "expression": "routine_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_company_responsible_user_idx": { + "name": "routine_runs_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idx": { + "name": "routine_runs_trigger_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_dispatch_fingerprint_idx": { + "name": "routine_runs_dispatch_fingerprint_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatch_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_linked_issue_idx": { + "name": "routine_runs_linked_issue_idx", + "columns": [ + { + "expression": "linked_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idempotency_idx": { + "name": "routine_runs_trigger_idempotency_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_runs_company_id_companies_id_fk": { + "name": "routine_runs_company_id_companies_id_fk", + "tableFrom": "routine_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_trigger_id_routine_triggers_id_fk": { + "name": "routine_runs_trigger_id_routine_triggers_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_routine_revision_id_routine_revisions_id_fk": { + "name": "routine_runs_routine_revision_id_routine_revisions_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_revisions", + "columnsFrom": [ + "routine_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_linked_issue_id_issues_id_fk": { + "name": "routine_runs_linked_issue_id_issues_id_fk", + "tableFrom": "routine_runs", + "tableTo": "issues", + "columnsFrom": [ + "linked_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_triggers": { + "name": "routine_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "signing_mode": { + "name": "signing_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replay_window_sec": { + "name": "replay_window_sec", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_triggers_company_routine_idx": { + "name": "routine_triggers_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_company_kind_idx": { + "name": "routine_triggers_company_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_next_run_idx": { + "name": "routine_triggers_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_idx": { + "name": "routine_triggers_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_uq": { + "name": "routine_triggers_public_id_uq", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_triggers_company_id_companies_id_fk": { + "name": "routine_triggers_company_id_companies_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_routine_id_routines_id_fk": { + "name": "routine_triggers_routine_id_routines_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_secret_id_company_secrets_id_fk": { + "name": "routine_triggers_secret_id_company_secrets_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_created_by_agent_id_agents_id_fk": { + "name": "routine_triggers_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_updated_by_agent_id_agents_id_fk": { + "name": "routine_triggers_updated_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'coalesce_if_active'" + }, + "catch_up_policy": { + "name": "catch_up_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip_missed'" + }, + "activity_gate_policy": { + "name": "activity_gate_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'always'" + }, + "activity_gate_scope": { + "name": "activity_gate_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_enqueued_at": { + "name": "last_enqueued_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": { + "routines_company_status_idx": { + "name": "routines_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_assignee_idx": { + "name": "routines_company_assignee_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_project_idx": { + "name": "routines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_folder_idx": { + "name": "routines_company_folder_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_responsible_user_idx": { + "name": "routines_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_origin_idx": { + "name": "routines_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routines_company_id_companies_id_fk": { + "name": "routines_company_id_companies_id_fk", + "tableFrom": "routines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_project_id_projects_id_fk": { + "name": "routines_project_id_projects_id_fk", + "tableFrom": "routines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_folder_id_folders_id_fk": { + "name": "routines_folder_id_folders_id_fk", + "tableFrom": "routines", + "tableTo": "folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_goal_id_goals_id_fk": { + "name": "routines_goal_id_goals_id_fk", + "tableFrom": "routines", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_parent_issue_id_issues_id_fk": { + "name": "routines_parent_issue_id_issues_id_fk", + "tableFrom": "routines", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_assignee_agent_id_agents_id_fk": { + "name": "routines_assignee_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routines_created_by_agent_id_agents_id_fk": { + "name": "routines_created_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_updated_by_agent_id_agents_id_fk": { + "name": "routines_updated_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_access_events": { + "name": "secret_access_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_secret_definition_id": { + "name": "user_secret_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_owner_user_id": { + "name": "credential_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_subject_type": { + "name": "credential_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_subject_id": { + "name": "credential_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumer_type": { + "name": "consumer_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer_id": { + "name": "consumer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_access_events_company_created_idx": { + "name": "secret_access_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_secret_created_idx": { + "name": "secret_access_events_secret_created_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_user_definition_created_idx": { + "name": "secret_access_events_user_definition_created_idx", + "columns": [ + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_company_credential_owner_idx": { + "name": "secret_access_events_company_credential_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_consumer_idx": { + "name": "secret_access_events_consumer_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_run_idx": { + "name": "secret_access_events_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_access_events_company_id_companies_id_fk": { + "name": "secret_access_events_company_id_companies_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "secret_access_events_secret_id_company_secrets_id_fk": { + "name": "secret_access_events_secret_id_company_secrets_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk": { + "name": "secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "user_secret_definitions", + "columnsFrom": [ + "user_secret_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "secret_access_events_issue_id_issues_id_fk": { + "name": "secret_access_events_issue_id_issues_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "secret_access_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "secret_access_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "secret_access_events_plugin_id_plugins_id_fk": { + "name": "secret_access_events_plugin_id_plugins_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.smoke_run_steps": { + "name": "smoke_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_step": { + "name": "scenario_step", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "screenshot_artifact_ref": { + "name": "screenshot_artifact_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_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": { + "smoke_run_steps_company_run_idx": { + "name": "smoke_run_steps_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "smoke_run_steps_company_path_idx": { + "name": "smoke_run_steps_company_path_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "smoke_run_steps_company_id_companies_id_fk": { + "name": "smoke_run_steps_company_id_companies_id_fk", + "tableFrom": "smoke_run_steps", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "smoke_run_steps_run_id_smoke_runs_id_fk": { + "name": "smoke_run_steps_run_id_smoke_runs_id_fk", + "tableFrom": "smoke_run_steps", + "tableTo": "smoke_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.smoke_runs": { + "name": "smoke_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "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": { + "smoke_runs_company_started_idx": { + "name": "smoke_runs_company_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "smoke_runs_company_status_idx": { + "name": "smoke_runs_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "smoke_runs_company_id_companies_id_fk": { + "name": "smoke_runs_company_id_companies_id_fk", + "tableFrom": "smoke_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_card_updates": { + "name": "status_card_updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "card_id": { + "name": "card_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_issue_id": { + "name": "generation_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "query_version": { + "name": "query_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "status_card_updates_card_started_idx": { + "name": "status_card_updates_card_started_idx", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "status_card_updates_generation_issue_idx": { + "name": "status_card_updates_generation_issue_idx", + "columns": [ + { + "expression": "generation_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "status_card_updates_card_id_status_cards_id_fk": { + "name": "status_card_updates_card_id_status_cards_id_fk", + "tableFrom": "status_card_updates", + "tableTo": "status_cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_card_updates_generation_issue_id_issues_id_fk": { + "name": "status_card_updates_generation_issue_id_issues_id_fk", + "tableFrom": "status_card_updates", + "tableTo": "issues", + "columnsFrom": [ + "generation_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_card_updates_run_id_heartbeat_runs_id_fk": { + "name": "status_card_updates_run_id_heartbeat_runs_id_fk", + "tableFrom": "status_card_updates", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_cards": { + "name": "status_cards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title_pinned": { + "name": "title_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "interest_prompt": { + "name": "interest_prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queries": { + "name": "queries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "query_version": { + "name": "query_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "query_compiled_at": { + "name": "query_compiled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "query_compiled_by_agent_id": { + "name": "query_compiled_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_policy": { + "name": "refresh_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compiling'" + }, + "pending_change_count": { + "name": "pending_change_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pending_change_hash": { + "name": "pending_change_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_change_at": { + "name": "last_change_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "fingerprint_at": { + "name": "fingerprint_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "mentioned_issue_ids": { + "name": "mentioned_issue_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_update_run_kind": { + "name": "last_update_run_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_generated_at": { + "name": "last_generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "generating_issue_id": { + "name": "generating_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_eval_at": { + "name": "next_eval_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_by_user_id": { + "name": "archived_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_agent_id": { + "name": "archived_by_agent_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": { + "status_cards_company_archived_idx": { + "name": "status_cards_company_archived_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "status_cards_company_next_eval_idx": { + "name": "status_cards_company_next_eval_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_eval_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "status_cards_company_id_companies_id_fk": { + "name": "status_cards_company_id_companies_id_fk", + "tableFrom": "status_cards", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_cards_created_by_agent_id_agents_id_fk": { + "name": "status_cards_created_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_query_compiled_by_agent_id_agents_id_fk": { + "name": "status_cards_query_compiled_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "query_compiled_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_agent_id_agents_id_fk": { + "name": "status_cards_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_document_id_documents_id_fk": { + "name": "status_cards_document_id_documents_id_fk", + "tableFrom": "status_cards", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_generating_issue_id_issues_id_fk": { + "name": "status_cards_generating_issue_id_issues_id_fk", + "tableFrom": "status_cards", + "tableTo": "issues", + "columnsFrom": [ + "generating_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_archived_by_agent_id_agents_id_fk": { + "name": "status_cards_archived_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "archived_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summary_slots": { + "name": "summary_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slot_key": { + "name": "slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "generating_issue_id": { + "name": "generating_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_generated_at": { + "name": "last_generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_generated_by_agent_id": { + "name": "last_generated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "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": { + "summary_slots_document_uq": { + "name": "summary_slots_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_slots_company_scope_idx": { + "name": "summary_slots_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_slots_company_generating_issue_idx": { + "name": "summary_slots_company_generating_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generating_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_slots_company_updated_idx": { + "name": "summary_slots_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "summary_slots_company_id_companies_id_fk": { + "name": "summary_slots_company_id_companies_id_fk", + "tableFrom": "summary_slots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "summary_slots_document_id_documents_id_fk": { + "name": "summary_slots_document_id_documents_id_fk", + "tableFrom": "summary_slots", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "summary_slots_generating_issue_id_issues_id_fk": { + "name": "summary_slots_generating_issue_id_issues_id_fk", + "tableFrom": "summary_slots", + "tableTo": "issues", + "columnsFrom": [ + "generating_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "summary_slots_last_generated_by_agent_id_agents_id_fk": { + "name": "summary_slots_last_generated_by_agent_id_agents_id_fk", + "tableFrom": "summary_slots", + "tableTo": "agents", + "columnsFrom": [ + "last_generated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "summary_slots_company_scope_slot_uq": { + "name": "summary_slots_company_scope_slot_uq", + "nullsNotDistinct": true, + "columns": [ + "company_id", + "scope_kind", + "scope_id", + "slot_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_access_audit_events": { + "name": "tool_access_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "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": { + "tool_access_audit_company_created_idx": { + "name": "tool_access_audit_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_access_audit_connection_idx": { + "name": "tool_access_audit_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_access_audit_gateway_idx": { + "name": "tool_access_audit_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_access_audit_events_company_id_companies_id_fk": { + "name": "tool_access_audit_events_company_id_companies_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_access_audit_events_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_access_audit_events_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_access_audit_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_access_audit_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_access_audit_events_connection_id_tool_connections_id_fk": { + "name": "tool_access_audit_events_connection_id_tool_connections_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_access_audit_events_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_access_audit_events_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_action_requests": { + "name": "tool_action_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_id": { + "name": "invocation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interaction_id": { + "name": "interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "canonical_arguments_hash": { + "name": "canonical_arguments_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_arguments_summary": { + "name": "canonical_arguments_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "signed_arguments": { + "name": "signed_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_markdown": { + "name": "preview_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_agent_id": { + "name": "decided_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "tool_action_requests_company_status_idx": { + "name": "tool_action_requests_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_action_requests_invocation_idx": { + "name": "tool_action_requests_invocation_idx", + "columns": [ + { + "expression": "invocation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_action_requests_issue_idx": { + "name": "tool_action_requests_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_action_requests_company_id_companies_id_fk": { + "name": "tool_action_requests_company_id_companies_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_action_requests_invocation_id_tool_invocations_id_fk": { + "name": "tool_action_requests_invocation_id_tool_invocations_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "tool_invocations", + "columnsFrom": [ + "invocation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_action_requests_issue_id_issues_id_fk": { + "name": "tool_action_requests_issue_id_issues_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_interaction_id_issue_thread_interactions_id_fk": { + "name": "tool_action_requests_interaction_id_issue_thread_interactions_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "issue_thread_interactions", + "columnsFrom": [ + "interaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_approval_id_approvals_id_fk": { + "name": "tool_action_requests_approval_id_approvals_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_requested_by_agent_id_agents_id_fk": { + "name": "tool_action_requests_requested_by_agent_id_agents_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_resolved_by_agent_id_agents_id_fk": { + "name": "tool_action_requests_resolved_by_agent_id_agents_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_decided_by_agent_id_agents_id_fk": { + "name": "tool_action_requests_decided_by_agent_id_agents_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "agents", + "columnsFrom": [ + "decided_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_applications": { + "name": "tool_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_key": { + "name": "application_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "tool_applications_company_idx": { + "name": "tool_applications_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_applications_company_status_idx": { + "name": "tool_applications_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_applications_company_name_uq": { + "name": "tool_applications_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_applications_company_key_uq": { + "name": "tool_applications_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "application_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_applications_company_id_companies_id_fk": { + "name": "tool_applications_company_id_companies_id_fk", + "tableFrom": "tool_applications", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_applications_plugin_id_plugins_id_fk": { + "name": "tool_applications_plugin_id_plugins_id_fk", + "tableFrom": "tool_applications", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_applications_owner_agent_id_agents_id_fk": { + "name": "tool_applications_owner_agent_id_agents_id_fk", + "tableFrom": "tool_applications", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_call_events": { + "name": "tool_call_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_type": { + "name": "client_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_id": { + "name": "client_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_session_id": { + "name": "mcp_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invocation_id": { + "name": "invocation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action_request_id": { + "name": "action_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_slot_id": { + "name": "runtime_slot_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_policy_ids": { + "name": "matched_policy_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy_explanation": { + "name": "policy_explanation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_scope_summary": { + "name": "credential_scope_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "header_policy_summary": { + "name": "header_policy_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "arguments_summary": { + "name": "arguments_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_summary": { + "name": "request_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_hash": { + "name": "result_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_size_bytes": { + "name": "result_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "redaction_plan": { + "name": "redaction_plan", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rate_limit_state": { + "name": "rate_limit_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "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()" + } + }, + "indexes": { + "tool_call_events_company_created_idx": { + "name": "tool_call_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_run_idx": { + "name": "tool_call_events_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_issue_idx": { + "name": "tool_call_events_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_invocation_idx": { + "name": "tool_call_events_invocation_idx", + "columns": [ + { + "expression": "invocation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_gateway_idx": { + "name": "tool_call_events_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_call_events_company_id_companies_id_fk": { + "name": "tool_call_events_company_id_companies_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_call_events_agent_id_agents_id_fk": { + "name": "tool_call_events_agent_id_agents_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_run_id_heartbeat_runs_id_fk": { + "name": "tool_call_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_issue_id_issues_id_fk": { + "name": "tool_call_events_issue_id_issues_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_call_events_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_call_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_application_id_tool_applications_id_fk": { + "name": "tool_call_events_application_id_tool_applications_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_connection_id_tool_connections_id_fk": { + "name": "tool_call_events_connection_id_tool_connections_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_call_events_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_invocation_id_tool_invocations_id_fk": { + "name": "tool_call_events_invocation_id_tool_invocations_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_invocations", + "columnsFrom": [ + "invocation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_action_request_id_tool_action_requests_id_fk": { + "name": "tool_call_events_action_request_id_tool_action_requests_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_action_requests", + "columnsFrom": [ + "action_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_runtime_slot_id_tool_runtime_slots_id_fk": { + "name": "tool_call_events_runtime_slot_id_tool_runtime_slots_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_runtime_slots", + "columnsFrom": [ + "runtime_slot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_catalog_entries": { + "name": "tool_catalog_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'tool'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "output_schema": { + "name": "output_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "annotations": { + "name": "annotations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read'" + }, + "is_read_only": { + "name": "is_read_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_write": { + "name": "is_write", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_destructive": { + "name": "is_destructive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version_hash": { + "name": "version_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_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()" + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_agent_id": { + "name": "reviewed_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_user_id": { + "name": "reviewed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quarantined_at": { + "name": "quarantined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "quarantine_reason": { + "name": "quarantine_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": { + "tool_catalog_entries_company_idx": { + "name": "tool_catalog_entries_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_application_idx": { + "name": "tool_catalog_entries_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_connection_idx": { + "name": "tool_catalog_entries_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_company_status_idx": { + "name": "tool_catalog_entries_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_connection_name_uq": { + "name": "tool_catalog_entries_connection_name_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_catalog_entries_company_id_companies_id_fk": { + "name": "tool_catalog_entries_company_id_companies_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_catalog_entries_application_id_tool_applications_id_fk": { + "name": "tool_catalog_entries_application_id_tool_applications_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_catalog_entries_connection_id_tool_connections_id_fk": { + "name": "tool_catalog_entries_connection_id_tool_connections_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_catalog_entries_reviewed_by_agent_id_agents_id_fk": { + "name": "tool_catalog_entries_reviewed_by_agent_id_agents_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "agents", + "columnsFrom": [ + "reviewed_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_connection_installs": { + "name": "tool_connection_installs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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": { + "tool_connection_installs_company_target_idx": { + "name": "tool_connection_installs_company_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connection_installs_connection_idx": { + "name": "tool_connection_installs_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connection_installs_target_uq": { + "name": "tool_connection_installs_target_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_connection_installs_company_id_companies_id_fk": { + "name": "tool_connection_installs_company_id_companies_id_fk", + "tableFrom": "tool_connection_installs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_connection_installs_connection_id_tool_connections_id_fk": { + "name": "tool_connection_installs_connection_id_tool_connections_id_fk", + "tableFrom": "tool_connection_installs", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_connection_installs_created_by_agent_id_agents_id_fk": { + "name": "tool_connection_installs_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_connection_installs", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "tool_connection_installs_target_type_check": { + "name": "tool_connection_installs_target_type_check", + "value": "\"tool_connection_installs\".\"target_type\" in ('company', 'agent')" + } + }, + "isRLSEnabled": false + }, + "public.tool_connections": { + "name": "tool_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'managed'" + }, + "ownership": { + "name": "ownership", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'customer'" + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "transport_config": { + "name": "transport_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "credential_refs": { + "name": "credential_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "credential_secret_refs": { + "name": "credential_secret_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unchecked'" + }, + "health_message": { + "name": "health_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_checked_at": { + "name": "health_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_health_at": { + "name": "last_health_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_catalog_refresh_at": { + "name": "last_catalog_refresh_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_connections_company_idx": { + "name": "tool_connections_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connections_application_idx": { + "name": "tool_connections_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connections_company_enabled_idx": { + "name": "tool_connections_company_enabled_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connections_company_uid_uq": { + "name": "tool_connections_company_uid_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_connections_company_id_companies_id_fk": { + "name": "tool_connections_company_id_companies_id_fk", + "tableFrom": "tool_connections", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_connections_application_id_tool_applications_id_fk": { + "name": "tool_connections_application_id_tool_applications_id_fk", + "tableFrom": "tool_connections", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tool_connections_created_by_agent_id_agents_id_fk": { + "name": "tool_connections_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_connections", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tool_connections_company_id_uq": { + "name": "tool_connections_company_id_uq", + "nullsNotDistinct": false, + "columns": [ + "company_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "tool_connections_ownership_check": { + "name": "tool_connections_ownership_check", + "value": "\"tool_connections\".\"ownership\" in ('platform_shared', 'platform_provisioned', 'customer', 'dcr')" + }, + "tool_connections_transport_check": { + "name": "tool_connections_transport_check", + "value": "\"tool_connections\".\"transport\" in ('mcp_remote', 'rest_api', 'local_stdio')" + }, + "tool_connections_auth_kind_check": { + "name": "tool_connections_auth_kind_check", + "value": "\"tool_connections\".\"auth_kind\" in ('oauth', 'api_key', 'none')" + } + }, + "isRLSEnabled": false + }, + "public.tool_gateway_rate_limit_counters": { + "name": "tool_gateway_rate_limit_counters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "counter_key": { + "name": "counter_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start_at": { + "name": "window_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_ms": { + "name": "window_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_at": { + "name": "reset_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": { + "tool_gateway_rate_limit_counters_company_idx": { + "name": "tool_gateway_rate_limit_counters_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_rate_limit_counters_window_uq": { + "name": "tool_gateway_rate_limit_counters_window_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "counter_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_gateway_rate_limit_counters_company_id_companies_id_fk": { + "name": "tool_gateway_rate_limit_counters_company_id_companies_id_fk", + "tableFrom": "tool_gateway_rate_limit_counters", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_gateway_sessions": { + "name": "tool_gateway_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_type": { + "name": "client_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_id": { + "name": "client_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_session_id": { + "name": "mcp_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_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": { + "tool_gateway_sessions_token_hash_uq": { + "name": "tool_gateway_sessions_token_hash_uq", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_company_agent_idx": { + "name": "tool_gateway_sessions_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_company_expires_idx": { + "name": "tool_gateway_sessions_company_expires_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_run_idx": { + "name": "tool_gateway_sessions_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_issue_idx": { + "name": "tool_gateway_sessions_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_gateway_idx": { + "name": "tool_gateway_sessions_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_gateway_sessions_company_id_companies_id_fk": { + "name": "tool_gateway_sessions_company_id_companies_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_gateway_sessions_agent_id_agents_id_fk": { + "name": "tool_gateway_sessions_agent_id_agents_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_gateway_sessions_run_id_heartbeat_runs_id_fk": { + "name": "tool_gateway_sessions_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_gateway_sessions_issue_id_issues_id_fk": { + "name": "tool_gateway_sessions_issue_id_issues_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_gateway_sessions_project_id_projects_id_fk": { + "name": "tool_gateway_sessions_project_id_projects_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_gateway_sessions_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_gateway_sessions_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_gateway_sessions_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_gateway_sessions_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_invocations": { + "name": "tool_invocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_type": { + "name": "client_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_id": { + "name": "client_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_session_id": { + "name": "mcp_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_version_hash": { + "name": "catalog_version_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "catalog_schema_hash": { + "name": "catalog_schema_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_key": { + "name": "application_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream_tool_name": { + "name": "upstream_tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "arguments_hash": { + "name": "arguments_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "arguments_summary": { + "name": "arguments_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "policy_decision": { + "name": "policy_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_policy_ids": { + "name": "matched_policy_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "policy_explanation": { + "name": "policy_explanation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_scope_summary": { + "name": "credential_scope_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "header_policy_summary": { + "name": "header_policy_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_required'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "upstream_request_id": { + "name": "upstream_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_hash": { + "name": "result_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_size_bytes": { + "name": "result_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "result_artifact_id": { + "name": "result_artifact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_invocations_company_created_idx": { + "name": "tool_invocations_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_run_idx": { + "name": "tool_invocations_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_issue_idx": { + "name": "tool_invocations_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_gateway_idx": { + "name": "tool_invocations_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_company_idempotency_uq": { + "name": "tool_invocations_company_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_invocations_company_id_companies_id_fk": { + "name": "tool_invocations_company_id_companies_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_invocations_agent_id_agents_id_fk": { + "name": "tool_invocations_agent_id_agents_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_issue_id_issues_id_fk": { + "name": "tool_invocations_issue_id_issues_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_run_id_heartbeat_runs_id_fk": { + "name": "tool_invocations_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_invocations_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_invocations_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_application_id_tool_applications_id_fk": { + "name": "tool_invocations_application_id_tool_applications_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_connection_id_tool_connections_id_fk": { + "name": "tool_invocations_connection_id_tool_connections_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_invocations_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_mcp_gateway_tokens": { + "name": "tool_mcp_gateway_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gateway_client'" + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_label": { + "name": "client_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "owner_note": { + "name": "owner_note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "allowed_actions": { + "name": "allowed_actions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"tools/list\",\"tools/call\"]'::jsonb" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expiry_override_reason": { + "name": "expiry_override_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_override_by_user_id": { + "name": "expiry_override_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_override_by_agent_id": { + "name": "expiry_override_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expiry_override_at": { + "name": "expiry_override_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 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_mcp_gateway_tokens_token_hash_uq": { + "name": "tool_mcp_gateway_tokens_token_hash_uq", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateway_tokens_gateway_idx": { + "name": "tool_mcp_gateway_tokens_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateway_tokens_subject_idx": { + "name": "tool_mcp_gateway_tokens_subject_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateway_tokens_company_expires_idx": { + "name": "tool_mcp_gateway_tokens_company_expires_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_mcp_gateway_tokens_company_id_companies_id_fk": { + "name": "tool_mcp_gateway_tokens_company_id_companies_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_mcp_gateway_tokens_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_mcp_gateway_tokens_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_mcp_gateway_tokens_expiry_override_by_agent_id_agents_id_fk": { + "name": "tool_mcp_gateway_tokens_expiry_override_by_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "agents", + "columnsFrom": [ + "expiry_override_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateway_tokens_created_by_agent_id_agents_id_fk": { + "name": "tool_mcp_gateway_tokens_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_mcp_gateways": { + "name": "tool_mcp_gateways", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gw_' || replace(gen_random_uuid()::text, '-', '')" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_slug": { + "name": "display_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "default_profile_mode": { + "name": "default_profile_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gateway_only'" + }, + "context_scope_type": { + "name": "context_scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "context_scope_id": { + "name": "context_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_issue_id": { + "name": "approval_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "auth_config": { + "name": "auth_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":1,\"bearer\":{\"enabled\":true,\"tokenPrefix\":\"pcgw\",\"defaultTtlSeconds\":7776000,\"requireFiniteExpiry\":true,\"longLivedTokenRequiresOverride\":true},\"oauth\":{\"enabled\":false,\"reservedFor\":\"v1_5\",\"dynamicClientRegistration\":false,\"authorizationCodePkce\":false}}'::jsonb" + }, + "header_policy": { + "name": "header_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":1,\"callerPassthrough\":{\"enabled\":false,\"allowedHeaders\":[]},\"staticHeaders\":[],\"generatedMetadata\":{\"enabled\":false,\"allowedHeaders\":[]},\"responseHeaders\":{\"forwardMcpRequiredHeaders\":true,\"forwardSafeCacheHeaders\":true}}'::jsonb" + }, + "metadata_policy": { + "name": "metadata_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":1,\"forwardCompanyId\":false,\"forwardGatewayId\":false,\"forwardProjectId\":false,\"forwardIssueId\":false,\"forwardAgentId\":false,\"forwardRunId\":false,\"forwardCorrelationId\":true}'::jsonb" + }, + "on_demand_tools_config": { + "name": "on_demand_tools_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"enabled\":false,\"searchToolName\":\"search_tools\",\"runToolName\":\"run_tool\"}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_mcp_gateways_company_idx": { + "name": "tool_mcp_gateways_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_company_status_idx": { + "name": "tool_mcp_gateways_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_profile_idx": { + "name": "tool_mcp_gateways_profile_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_public_id_uq": { + "name": "tool_mcp_gateways_public_id_uq", + "columns": [ + { + "expression": "gateway_public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_company_slug_uq": { + "name": "tool_mcp_gateways_company_slug_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_company_name_uq": { + "name": "tool_mcp_gateways_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_mcp_gateways_company_id_companies_id_fk": { + "name": "tool_mcp_gateways_company_id_companies_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_mcp_gateways_profile_id_tool_profiles_id_fk": { + "name": "tool_mcp_gateways_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "tool_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "tool_mcp_gateways_agent_id_agents_id_fk": { + "name": "tool_mcp_gateways_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_project_id_projects_id_fk": { + "name": "tool_mcp_gateways_project_id_projects_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_issue_id_issues_id_fk": { + "name": "tool_mcp_gateways_issue_id_issues_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_approval_issue_id_issues_id_fk": { + "name": "tool_mcp_gateways_approval_issue_id_issues_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "issues", + "columnsFrom": [ + "approval_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_created_by_agent_id_agents_id_fk": { + "name": "tool_mcp_gateways_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_oauth_states": { + "name": "tool_oauth_states", + "schema": "", + "columns": { + "state": { + "name": "state", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_session_id": { + "name": "created_by_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_scopes": { + "name": "requested_scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interaction_id": { + "name": "interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_oauth_states_company_idx": { + "name": "tool_oauth_states_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_oauth_states_connection_idx": { + "name": "tool_oauth_states_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_oauth_states_actor_idx": { + "name": "tool_oauth_states_actor_idx", + "columns": [ + { + "expression": "created_by_actor_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_oauth_states_expires_at_idx": { + "name": "tool_oauth_states_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_oauth_states_company_id_companies_id_fk": { + "name": "tool_oauth_states_company_id_companies_id_fk", + "tableFrom": "tool_oauth_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_oauth_states_connection_id_tool_connections_id_fk": { + "name": "tool_oauth_states_connection_id_tool_connections_id_fk", + "tableFrom": "tool_oauth_states", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_policies": { + "name": "tool_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_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 + }, + "policy_type": { + "name": "policy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "selectors": { + "name": "selectors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_policies_company_enabled_idx": { + "name": "tool_policies_company_enabled_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_policies_company_type_idx": { + "name": "tool_policies_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "policy_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_policies_company_name_uq": { + "name": "tool_policies_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_policies_company_id_companies_id_fk": { + "name": "tool_policies_company_id_companies_id_fk", + "tableFrom": "tool_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_policies_created_by_agent_id_agents_id_fk": { + "name": "tool_policies_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_policies", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_profile_bindings": { + "name": "tool_profile_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_profile_bindings_company_target_idx": { + "name": "tool_profile_bindings_company_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_bindings_target_profile_uq": { + "name": "tool_profile_bindings_target_profile_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_profile_bindings_company_id_companies_id_fk": { + "name": "tool_profile_bindings_company_id_companies_id_fk", + "tableFrom": "tool_profile_bindings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_bindings_profile_id_tool_profiles_id_fk": { + "name": "tool_profile_bindings_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_profile_bindings", + "tableTo": "tool_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_bindings_created_by_agent_id_agents_id_fk": { + "name": "tool_profile_bindings_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_profile_bindings", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_profile_entries": { + "name": "tool_profile_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selector_type": { + "name": "selector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'include'" + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "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": { + "tool_profile_entries_company_profile_idx": { + "name": "tool_profile_entries_company_profile_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_entries_application_idx": { + "name": "tool_profile_entries_application_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_entries_connection_idx": { + "name": "tool_profile_entries_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_entries_catalog_entry_idx": { + "name": "tool_profile_entries_catalog_entry_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog_entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_profile_entries_company_id_companies_id_fk": { + "name": "tool_profile_entries_company_id_companies_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_profile_id_tool_profiles_id_fk": { + "name": "tool_profile_entries_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_application_id_tool_applications_id_fk": { + "name": "tool_profile_entries_application_id_tool_applications_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_connection_id_tool_connections_id_fk": { + "name": "tool_profile_entries_connection_id_tool_connections_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_profile_entries_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_profiles": { + "name": "tool_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_key": { + "name": "profile_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "default_action": { + "name": "default_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'deny'" + }, + "new_tools_reviewed_at": { + "name": "new_tools_reviewed_at", + "type": "timestamp with time zone", + "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": { + "tool_profiles_company_status_idx": { + "name": "tool_profiles_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profiles_company_key_uq": { + "name": "tool_profiles_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profiles_company_name_uq": { + "name": "tool_profiles_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_profiles_company_id_companies_id_fk": { + "name": "tool_profiles_company_id_companies_id_fk", + "tableFrom": "tool_profiles", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_rate_limit_counters": { + "name": "tool_rate_limit_counters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "counter_key": { + "name": "counter_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start_at": { + "name": "window_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reset_at": { + "name": "reset_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": { + "tool_rate_limit_counters_company_idx": { + "name": "tool_rate_limit_counters_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_rate_limit_counters_window_uq": { + "name": "tool_rate_limit_counters_window_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "counter_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_rate_limit_counters_company_id_companies_id_fk": { + "name": "tool_rate_limit_counters_company_id_companies_id_fk", + "tableFrom": "tool_rate_limit_counters", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_rate_limit_counters_policy_id_tool_policies_id_fk": { + "name": "tool_rate_limit_counters_policy_id_tool_policies_id_fk", + "tableFrom": "tool_rate_limit_counters", + "tableTo": "tool_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_runtime_metric_counters": { + "name": "tool_runtime_metric_counters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket_start_at": { + "name": "bucket_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "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": { + "tool_runtime_metric_counters_company_metric_idx": { + "name": "tool_runtime_metric_counters_company_metric_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bucket_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_metric_counters_bucket_uq": { + "name": "tool_runtime_metric_counters_bucket_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bucket_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_runtime_metric_counters_company_id_companies_id_fk": { + "name": "tool_runtime_metric_counters_company_id_companies_id_fk", + "tableFrom": "tool_runtime_metric_counters", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_runtime_slots": { + "name": "tool_runtime_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_scope_type": { + "name": "owner_scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connection'" + }, + "owner_scope_id": { + "name": "owner_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_kind": { + "name": "runtime_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_stdio'" + }, + "slot_key": { + "name": "slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stopped'" + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_scope": { + "name": "workspace_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_scope_hash": { + "name": "credential_scope_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_id": { + "name": "process_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "command_template_key": { + "name": "command_template_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unchecked'" + }, + "health_message": { + "name": "health_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_health_check_at": { + "name": "last_health_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stopped_at": { + "name": "stopped_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 + }, + "idle_expires_at": { + "name": "idle_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "idle_deadline_at": { + "name": "idle_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "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": { + "tool_runtime_slots_company_idx": { + "name": "tool_runtime_slots_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_slots_connection_idx": { + "name": "tool_runtime_slots_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_slots_execution_workspace_idx": { + "name": "tool_runtime_slots_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_slots_slot_key_uq": { + "name": "tool_runtime_slots_slot_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slot_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_runtime_slots_company_id_companies_id_fk": { + "name": "tool_runtime_slots_company_id_companies_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_runtime_slots_application_id_tool_applications_id_fk": { + "name": "tool_runtime_slots_application_id_tool_applications_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_runtime_slots_connection_id_tool_connections_id_fk": { + "name": "tool_runtime_slots_connection_id_tool_connections_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_runtime_slots_project_workspace_id_project_workspaces_id_fk": { + "name": "tool_runtime_slots_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_runtime_slots_execution_workspace_id_execution_workspaces_id_fk": { + "name": "tool_runtime_slots_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_runtime_slots_issue_id_issues_id_fk": { + "name": "tool_runtime_slots_issue_id_issues_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_stdio_command_templates": { + "name": "tool_stdio_command_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "env_keys": { + "name": "env_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_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": { + "tool_stdio_command_templates_company_idx": { + "name": "tool_stdio_command_templates_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_stdio_command_templates_company_status_idx": { + "name": "tool_stdio_command_templates_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_stdio_command_templates_company_key_uq": { + "name": "tool_stdio_command_templates_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_stdio_command_templates_company_id_companies_id_fk": { + "name": "tool_stdio_command_templates_company_id_companies_id_fk", + "tableFrom": "tool_stdio_command_templates", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_stdio_command_templates_created_by_agent_id_agents_id_fk": { + "name": "tool_stdio_command_templates_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_stdio_command_templates", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_inbox_agent_policies": { + "name": "user_inbox_agent_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "allowed_agent_ids": { + "name": "allowed_agent_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_inbox_agent_policies_company_user_uq": { + "name": "user_inbox_agent_policies_company_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_inbox_agent_policies_allowed_agent_ids_idx": { + "name": "user_inbox_agent_policies_allowed_agent_ids_idx", + "columns": [ + { + "expression": "allowed_agent_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "user_inbox_agent_policies_company_id_companies_id_fk": { + "name": "user_inbox_agent_policies_company_id_companies_id_fk", + "tableFrom": "user_inbox_agent_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_inbox_agent_policies_mode_check": { + "name": "user_inbox_agent_policies_mode_check", + "value": "\"user_inbox_agent_policies\".\"mode\" in ('open', 'allowlist', 'disabled')" + } + }, + "isRLSEnabled": false + }, + "public.user_secret_declarations": { + "name": "user_secret_declarations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_secret_definition_id": { + "name": "user_secret_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_selector": { + "name": "version_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'latest'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_missing_override": { + "name": "allow_missing_override", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "label": { + "name": "label", + "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": { + "user_secret_declarations_company_idx": { + "name": "user_secret_declarations_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_definition_idx": { + "name": "user_secret_declarations_definition_idx", + "columns": [ + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_target_idx": { + "name": "user_secret_declarations_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_company_required_idx": { + "name": "user_secret_declarations_company_required_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "required", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_target_path_uq": { + "name": "user_secret_declarations_target_path_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_required_override_idx": { + "name": "user_secret_declarations_required_override_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "allow_missing_override", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_secret_declarations\".\"allow_missing_override\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_secret_declarations_company_id_companies_id_fk": { + "name": "user_secret_declarations_company_id_companies_id_fk", + "tableFrom": "user_secret_declarations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk": { + "name": "user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk", + "tableFrom": "user_secret_declarations", + "tableTo": "user_secret_definitions", + "columnsFrom": [ + "user_secret_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_secret_definitions": { + "name": "user_secret_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "managed_mode": { + "name": "managed_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip_managed'" + }, + "provider_config_id": { + "name": "provider_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "usage_guidance": { + "name": "usage_guidance", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "user_secret_definitions_company_status_idx": { + "name": "user_secret_definitions_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_definitions_company_provider_idx": { + "name": "user_secret_definitions_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_definitions_provider_config_idx": { + "name": "user_secret_definitions_provider_config_idx", + "columns": [ + { + "expression": "provider_config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_definitions_company_key_uq": { + "name": "user_secret_definitions_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_secret_definitions\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_secret_definitions_company_id_companies_id_fk": { + "name": "user_secret_definitions_company_id_companies_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk": { + "name": "user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "company_secret_provider_configs", + "columnsFrom": [ + "provider_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_secret_definitions_created_by_agent_id_agents_id_fk": { + "name": "user_secret_definitions_created_by_agent_id_agents_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_secret_definitions_updated_by_agent_id_agents_id_fk": { + "name": "user_secret_definitions_updated_by_agent_id_agents_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sidebar_preferences": { + "name": "user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_order": { + "name": "company_order", + "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": { + "user_sidebar_preferences_user_uq": { + "name": "user_sidebar_preferences_user_uq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operations": { + "name": "workspace_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_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": { + "workspace_operations_company_run_started_idx": { + "name": "workspace_operations_company_run_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_started_idx": { + "name": "workspace_operations_company_workspace_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_issue_started_idx": { + "name": "workspace_operations_company_workspace_issue_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_issue_id_issues_id_fk": { + "name": "workspace_operations_issue_id_issues_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_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": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exposure_handle": { + "name": "exposure_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backend_url": { + "name": "backend_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_execution_workspace_status_idx": { + "name": "workspace_runtime_services_company_execution_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0222_snapshot.json b/packages/db/src/migrations/meta/0222_snapshot.json new file mode 100644 index 0000000000..ffe12fee54 --- /dev/null +++ b/packages/db/src/migrations/meta/0222_snapshot.json @@ -0,0 +1,39714 @@ +{ + "id": "0df620d7-2e5a-4304-a98e-36c8bc550010", + "prevId": "8f54003b-ed12-4ab6-a3f3-c30b4284bcc7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_company_agent_created_idx": { + "name": "activity_log_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_company_responsible_user_created_idx": { + "name": "activity_log_company_responsible_user_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.adapter_auth_sessions": { + "name": "adapter_auth_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'starting'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "promotion_expires_at": { + "name": "promotion_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_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": { + "adapter_auth_sessions_company_status_idx": { + "name": "adapter_auth_sessions_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_company_adapter_active_uq": { + "name": "adapter_auth_sessions_company_adapter_active_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"adapter_auth_sessions\".\"status\" IN ('starting', 'waiting_for_user', 'promoting')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_environment_idx": { + "name": "adapter_auth_sessions_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_expires_idx": { + "name": "adapter_auth_sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "adapter_auth_sessions_provider_lease_idx": { + "name": "adapter_auth_sessions_provider_lease_idx", + "columns": [ + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "adapter_auth_sessions_company_id_companies_id_fk": { + "name": "adapter_auth_sessions_company_id_companies_id_fk", + "tableFrom": "adapter_auth_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "adapter_auth_sessions_environment_id_environments_id_fk": { + "name": "adapter_auth_sessions_environment_id_environments_id_fk", + "tableFrom": "adapter_auth_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_config": { + "name": "scope_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_memberships": { + "name": "agent_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'joined'" + }, + "starred_at": { + "name": "starred_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": { + "agent_memberships_company_user_idx": { + "name": "agent_memberships_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memberships_company_user_starred_idx": { + "name": "agent_memberships_company_user_starred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memberships_agent_idx": { + "name": "agent_memberships_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memberships_company_user_agent_uq": { + "name": "agent_memberships_company_user_agent_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memberships_company_id_companies_id_fk": { + "name": "agent_memberships_company_id_companies_id_fk", + "tableFrom": "agent_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memberships_agent_id_agents_id_fk": { + "name": "agent_memberships_agent_id_agents_id_fk", + "tableFrom": "agent_memberships", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_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 + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_review_path_recovery_idempotency_uq": { + "name": "agent_wakeup_requests_review_path_recovery_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_wakeup_requests\".\"idempotency_key\" LIKE 'issue_review_path_lost:%' AND \"agent_wakeup_requests\".\"status\" <> 'skipped'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_payload_issue_idx": { + "name": "agent_wakeup_requests_company_payload_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"payload\" ->> 'issueId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_reason": { + "name": "error_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_default_environment_idx": { + "name": "agents_company_default_environment_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "default_environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_default_environment_id_environments_id_fk": { + "name": "agents_default_environment_id_environments_id_fk", + "tableFrom": "agents", + "tableTo": "environments", + "columnsFrom": [ + "default_environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_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": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_api_keys": { + "name": "board_api_keys", + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_api_keys_key_hash_idx": { + "name": "board_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_api_keys_user_idx": { + "name": "board_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_api_keys_user_id_user_id_fk": { + "name": "board_api_keys_user_id_user_id_fk", + "tableFrom": "board_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "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 + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.built_in_managed_resources": { + "name": "built_in_managed_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bundle_key": { + "name": "bundle_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_kind": { + "name": "resource_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stock_version": { + "name": "stock_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stock_hash": { + "name": "stock_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "defaults_json": { + "name": "defaults_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": { + "built_in_managed_resources_company_idx": { + "name": "built_in_managed_resources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "built_in_managed_resources_resource_idx": { + "name": "built_in_managed_resources_resource_idx", + "columns": [ + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "built_in_managed_resources_company_bundle_resource_uq": { + "name": "built_in_managed_resources_company_bundle_resource_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bundle_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "built_in_managed_resources_company_id_companies_id_fk": { + "name": "built_in_managed_resources_company_id_companies_id_fk", + "tableFrom": "built_in_managed_resources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.case_attachments": { + "name": "case_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_attachments_company_case_idx": { + "name": "case_attachments_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_attachments_asset_uq": { + "name": "case_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_attachments_company_id_companies_id_fk": { + "name": "case_attachments_company_id_companies_id_fk", + "tableFrom": "case_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_attachments_case_id_cases_id_fk": { + "name": "case_attachments_case_id_cases_id_fk", + "tableFrom": "case_attachments", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_attachments_asset_id_assets_id_fk": { + "name": "case_attachments_asset_id_assets_id_fk", + "tableFrom": "case_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.case_documents": { + "name": "case_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "case_documents_company_case_key_uq": { + "name": "case_documents_company_case_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_documents_document_uq": { + "name": "case_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_documents_company_case_updated_idx": { + "name": "case_documents_company_case_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_documents_company_id_companies_id_fk": { + "name": "case_documents_company_id_companies_id_fk", + "tableFrom": "case_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_documents_case_id_cases_id_fk": { + "name": "case_documents_case_id_cases_id_fk", + "tableFrom": "case_documents", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_documents_document_id_documents_id_fk": { + "name": "case_documents_document_id_documents_id_fk", + "tableFrom": "case_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.case_events": { + "name": "case_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_events_case_created_idx": { + "name": "case_events_case_created_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_events_company_case_idx": { + "name": "case_events_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_events_company_id_companies_id_fk": { + "name": "case_events_company_id_companies_id_fk", + "tableFrom": "case_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_events_case_id_cases_id_fk": { + "name": "case_events_case_id_cases_id_fk", + "tableFrom": "case_events", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_events_actor_agent_id_agents_id_fk": { + "name": "case_events_actor_agent_id_agents_id_fk", + "tableFrom": "case_events", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "case_events_kind_check": { + "name": "case_events_kind_check", + "value": "\"case_events\".\"kind\" in (\n 'created',\n 'updated',\n 'fields_changed',\n 'status_changed',\n 'issue_linked',\n 'issue_unlinked',\n 'document_revised',\n 'child_linked',\n 'attachment_added',\n 'label_added',\n 'label_removed'\n )" + }, + "case_events_actor_type_check": { + "name": "case_events_actor_type_check", + "value": "\"case_events\".\"actor_type\" in ('user', 'agent', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.case_issue_links": { + "name": "case_issue_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_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": { + "case_issue_links_case_issue_uq": { + "name": "case_issue_links_case_issue_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_issue_links_company_case_idx": { + "name": "case_issue_links_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_issue_links_issue_idx": { + "name": "case_issue_links_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_issue_links_company_id_companies_id_fk": { + "name": "case_issue_links_company_id_companies_id_fk", + "tableFrom": "case_issue_links", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_issue_links_case_id_cases_id_fk": { + "name": "case_issue_links_case_id_cases_id_fk", + "tableFrom": "case_issue_links", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_issue_links_issue_id_issues_id_fk": { + "name": "case_issue_links_issue_id_issues_id_fk", + "tableFrom": "case_issue_links", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "case_issue_links_role_check": { + "name": "case_issue_links_role_check", + "value": "\"case_issue_links\".\"role\" in ('origin', 'work', 'reference')" + } + }, + "isRLSEnabled": false + }, + "public.case_labels": { + "name": "case_labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "case_labels_case_label_uq": { + "name": "case_labels_case_label_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_labels_company_case_idx": { + "name": "case_labels_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "case_labels_label_idx": { + "name": "case_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "case_labels_company_id_companies_id_fk": { + "name": "case_labels_company_id_companies_id_fk", + "tableFrom": "case_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_labels_case_id_cases_id_fk": { + "name": "case_labels_case_id_cases_id_fk", + "tableFrom": "case_labels", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "case_labels_label_id_labels_id_fk": { + "name": "case_labels_label_id_labels_id_fk", + "tableFrom": "case_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cases": { + "name": "cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "case_number": { + "name": "case_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "case_type": { + "name": "case_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "parent_case_id": { + "name": "parent_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "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": { + "cases_company_case_number_uq": { + "name": "cases_company_case_number_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_identifier_uq": { + "name": "cases_identifier_uq", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_type_key_uq": { + "name": "cases_company_type_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_status_idx": { + "name": "cases_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_type_idx": { + "name": "cases_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_company_project_idx": { + "name": "cases_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_parent_idx": { + "name": "cases_parent_idx", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cases_title_search_idx": { + "name": "cases_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "cases_identifier_search_idx": { + "name": "cases_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "cases_summary_search_idx": { + "name": "cases_summary_search_idx", + "columns": [ + { + "expression": "summary", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "cases_company_id_companies_id_fk": { + "name": "cases_company_id_companies_id_fk", + "tableFrom": "cases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cases_project_id_projects_id_fk": { + "name": "cases_project_id_projects_id_fk", + "tableFrom": "cases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cases_parent_case_id_cases_id_fk": { + "name": "cases_parent_case_id_cases_id_fk", + "tableFrom": "cases", + "tableTo": "cases", + "columnsFrom": [ + "parent_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cases_created_by_agent_id_agents_id_fk": { + "name": "cases_created_by_agent_id_agents_id_fk", + "tableFrom": "cases", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cases_status_check": { + "name": "cases_status_check", + "value": "\"cases\".\"status\" in ('draft', 'in_progress', 'in_review', 'approved', 'done', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.claude_setup_token_sessions": { + "name": "claude_setup_token_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "lease_id": { + "name": "lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'starting'" + }, + "deadline_at": { + "name": "deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "bound_at": { + "name": "bound_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": { + "claude_setup_token_sessions_active_uq": { + "name": "claude_setup_token_sessions_active_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"claude_setup_token_sessions\".\"state\" IN ('starting', 'awaiting_code', 'submitting', 'persisting')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "claude_setup_token_sessions_session_id_uq": { + "name": "claude_setup_token_sessions_session_id_uq", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "claude_setup_token_sessions_deadline_idx": { + "name": "claude_setup_token_sessions_deadline_idx", + "columns": [ + { + "expression": "deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "claude_setup_token_sessions_company_id_companies_id_fk": { + "name": "claude_setup_token_sessions_company_id_companies_id_fk", + "tableFrom": "claude_setup_token_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "claude_setup_token_sessions_environment_id_environments_id_fk": { + "name": "claude_setup_token_sessions_environment_id_environments_id_fk", + "tableFrom": "claude_setup_token_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_auth_challenges": { + "name": "cli_auth_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_access": { + "name": "requested_access", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'board'" + }, + "requested_company_id": { + "name": "requested_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pending_key_hash": { + "name": "pending_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_key_name": { + "name": "pending_key_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_api_key_id": { + "name": "board_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cli_auth_challenges_secret_hash_idx": { + "name": "cli_auth_challenges_secret_hash_idx", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_approved_by_idx": { + "name": "cli_auth_challenges_approved_by_idx", + "columns": [ + { + "expression": "approved_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_requested_company_idx": { + "name": "cli_auth_challenges_requested_company_idx", + "columns": [ + { + "expression": "requested_company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_auth_challenges_requested_company_id_companies_id_fk": { + "name": "cli_auth_challenges_requested_company_id_companies_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "companies", + "columnsFrom": [ + "requested_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_approved_by_user_id_user_id_fk": { + "name": "cli_auth_challenges_approved_by_user_id_user_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "user", + "columnsFrom": [ + "approved_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk": { + "name": "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "board_api_keys", + "columnsFrom": [ + "board_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attachment_max_bytes": { + "name": "attachment_max_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10485760 + }, + "default_responsible_user_id": { + "name": "default_responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "interaction_resolver_governance": { + "name": "interaction_resolver_governance", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "feedback_data_sharing_enabled": { + "name": "feedback_data_sharing_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_consent_at": { + "name": "feedback_data_sharing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_consent_by_user_id": { + "name": "feedback_data_sharing_consent_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_terms_version": { + "name": "feedback_data_sharing_terms_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "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": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "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": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_onboarding_seeds": { + "name": "company_onboarding_seeds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mission": { + "name": "mission", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_role": { + "name": "agent_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_task_title": { + "name": "first_task_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_task_details": { + "name": "first_task_details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_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": { + "company_onboarding_seeds_company_uq": { + "name": "company_onboarding_seeds_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_onboarding_seeds_company_id_companies_id_fk": { + "name": "company_onboarding_seeds_company_id_companies_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_onboarding_seeds_goal_id_goals_id_fk": { + "name": "company_onboarding_seeds_goal_id_goals_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_onboarding_seeds_agent_id_agents_id_fk": { + "name": "company_onboarding_seeds_agent_id_agents_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_onboarding_seeds_issue_id_issues_id_fk": { + "name": "company_onboarding_seeds_issue_id_issues_id_fk", + "tableFrom": "company_onboarding_seeds", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_bindings": { + "name": "company_secret_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_selector": { + "name": "version_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'latest'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projection_class": { + "name": "projection_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unclassified'" + }, + "projection_allowlist_key": { + "name": "projection_allowlist_key", + "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": { + "company_secret_bindings_company_idx": { + "name": "company_secret_bindings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_bindings_secret_idx": { + "name": "company_secret_bindings_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_bindings_target_idx": { + "name": "company_secret_bindings_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_bindings_target_path_uq": { + "name": "company_secret_bindings_target_path_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_bindings_company_id_companies_id_fk": { + "name": "company_secret_bindings_company_id_companies_id_fk", + "tableFrom": "company_secret_bindings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secret_bindings_secret_id_company_secrets_id_fk": { + "name": "company_secret_bindings_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_bindings", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_proposals": { + "name": "company_secret_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "proposed_name": { + "name": "proposed_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed_key": { + "name": "proposed_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposed_description": { + "name": "proposed_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "justification": { + "name": "justification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_ciphertext": { + "name": "value_ciphertext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "value_fingerprint_sha256": { + "name": "value_fingerprint_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value_length": { + "name": "value_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "secret_proposal_id": { + "name": "secret_proposal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projection_class": { + "name": "projection_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unclassified'" + }, + "binding_target_policy_snapshot": { + "name": "binding_target_policy_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposer_ancestor_ids_snapshot": { + "name": "proposer_ancestor_ids_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_ancestor_ids_snapshot": { + "name": "target_ancestor_ids_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "proposed_by_agent_id": { + "name": "proposed_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_secret_id": { + "name": "created_secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applied_binding_config_path": { + "name": "applied_binding_config_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ciphertext_scrubbed_at": { + "name": "ciphertext_scrubbed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secret_proposals_company_status_idx": { + "name": "company_secret_proposals_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_proposals_proposer_status_idx": { + "name": "company_secret_proposals_proposer_status_idx", + "columns": [ + { + "expression": "proposed_by_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_proposals_expiry_idx": { + "name": "company_secret_proposals_expiry_idx", + "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": {} + }, + "company_secret_proposals_secret_proposal_idx": { + "name": "company_secret_proposals_secret_proposal_idx", + "columns": [ + { + "expression": "secret_proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_proposals_company_id_companies_id_fk": { + "name": "company_secret_proposals_company_id_companies_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secret_proposals_secret_id_company_secrets_id_fk": { + "name": "company_secret_proposals_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secret_proposals_secret_proposal_id_company_secret_proposals_id_fk": { + "name": "company_secret_proposals_secret_proposal_id_company_secret_proposals_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "company_secret_proposals", + "columnsFrom": [ + "secret_proposal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_target_id_agents_id_fk": { + "name": "company_secret_proposals_target_id_agents_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "agents", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_proposed_by_agent_id_agents_id_fk": { + "name": "company_secret_proposals_proposed_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "agents", + "columnsFrom": [ + "proposed_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_origin_issue_id_issues_id_fk": { + "name": "company_secret_proposals_origin_issue_id_issues_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "issues", + "columnsFrom": [ + "origin_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secret_proposals_origin_run_id_heartbeat_runs_id_fk": { + "name": "company_secret_proposals_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "origin_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_proposals_created_secret_id_company_secrets_id_fk": { + "name": "company_secret_proposals_created_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_proposals", + "tableTo": "company_secrets", + "columnsFrom": [ + "created_secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "company_secret_proposals_kind_check": { + "name": "company_secret_proposals_kind_check", + "value": "\"company_secret_proposals\".\"kind\" in ('secret', 'binding')" + }, + "company_secret_proposals_status_check": { + "name": "company_secret_proposals_status_check", + "value": "\"company_secret_proposals\".\"status\" in ('pending', 'approved', 'rejected', 'withdrawn', 'expired')" + }, + "company_secret_proposals_projection_check": { + "name": "company_secret_proposals_projection_check", + "value": "\"company_secret_proposals\".\"projection_class\" = 'unclassified'" + }, + "company_secret_proposals_shape_check": { + "name": "company_secret_proposals_shape_check", + "value": "(\n \"company_secret_proposals\".\"kind\" = 'secret'\n and \"company_secret_proposals\".\"proposed_name\" is not null\n and \"company_secret_proposals\".\"proposed_key\" is not null\n and \"company_secret_proposals\".\"secret_id\" is null\n and \"company_secret_proposals\".\"secret_proposal_id\" is null\n and \"company_secret_proposals\".\"target_type\" is null\n and \"company_secret_proposals\".\"target_id\" is null\n and \"company_secret_proposals\".\"config_path\" is null\n ) or (\n \"company_secret_proposals\".\"kind\" = 'binding'\n and ((\"company_secret_proposals\".\"secret_id\" is not null)::int + (\"company_secret_proposals\".\"secret_proposal_id\" is not null)::int) = 1\n and \"company_secret_proposals\".\"target_type\" = 'agent'\n and \"company_secret_proposals\".\"target_id\" is not null\n and \"company_secret_proposals\".\"config_path\" is not null\n )" + } + }, + "isRLSEnabled": false + }, + "public.company_secret_provider_configs": { + "name": "company_secret_provider_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_checked_at": { + "name": "health_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "health_message": { + "name": "health_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_details": { + "name": "health_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secret_provider_configs_company_idx": { + "name": "company_secret_provider_configs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_provider_configs_company_provider_idx": { + "name": "company_secret_provider_configs_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_provider_configs_default_uq": { + "name": "company_secret_provider_configs_default_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secret_provider_configs\".\"is_default\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_provider_configs_company_id_companies_id_fk": { + "name": "company_secret_provider_configs_company_id_companies_id_fk", + "tableFrom": "company_secret_provider_configs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_provider_configs_created_by_agent_id_agents_id_fk": { + "name": "company_secret_provider_configs_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_provider_configs", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_version_ref": { + "name": "provider_version_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'current'" + }, + "fingerprint_sha256": { + "name": "fingerprint_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rotation_job_id": { + "name": "rotation_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_fingerprint_idx": { + "name": "company_secret_versions_fingerprint_idx", + "columns": [ + { + "expression": "fingerprint_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_secret_definition_id": { + "name": "user_secret_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "managed_mode": { + "name": "managed_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip_managed'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config_id": { + "name": "provider_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_resolved_at": { + "name": "last_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_scope_idx": { + "name": "company_secrets_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_owner_idx": { + "name": "company_secrets_company_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_user_definition_owner_idx": { + "name": "company_secrets_user_definition_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_provider_config_idx": { + "name": "company_secrets_provider_config_idx", + "columns": [ + { + "expression": "provider_config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'company' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_key_uq": { + "name": "company_secrets_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'company' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_user_definition_owner_uq": { + "name": "company_secrets_user_definition_owner_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_secrets\".\"scope\" = 'user' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk": { + "name": "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk", + "tableFrom": "company_secrets", + "tableTo": "user_secret_definitions", + "columnsFrom": [ + "user_secret_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secrets_provider_config_id_company_secret_provider_configs_id_fk": { + "name": "company_secrets_provider_config_id_company_secret_provider_configs_id_fk", + "tableFrom": "company_secrets", + "tableTo": "company_secret_provider_configs", + "columnsFrom": [ + "provider_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "company_secrets_scope_shape_check": { + "name": "company_secrets_scope_shape_check", + "value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n )" + } + }, + "isRLSEnabled": false + }, + "public.company_skill_policies": { + "name": "company_skill_policies", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "default_effect": { + "name": "default_effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rules": { + "name": "rules", + "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": {}, + "foreignKeys": { + "company_skill_policies_company_id_companies_id_fk": { + "name": "company_skill_policies_company_id_companies_id_fk", + "tableFrom": "company_skill_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_comments": { + "name": "company_skill_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_skill_id": { + "name": "company_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "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": { + "company_skill_comments_company_skill_created_idx": { + "name": "company_skill_comments_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_comments_parent_idx": { + "name": "company_skill_comments_parent_idx", + "columns": [ + { + "expression": "parent_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_comments_company_id_companies_id_fk": { + "name": "company_skill_comments_company_id_companies_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_comments_company_skill_id_company_skills_id_fk": { + "name": "company_skill_comments_company_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "company_skills", + "columnsFrom": [ + "company_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_comments_parent_comment_id_company_skill_comments_id_fk": { + "name": "company_skill_comments_parent_comment_id_company_skill_comments_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "company_skill_comments", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skill_comments_author_agent_id_agents_id_fk": { + "name": "company_skill_comments_author_agent_id_agents_id_fk", + "tableFrom": "company_skill_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_stars": { + "name": "company_skill_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_skill_id": { + "name": "company_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "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": { + "company_skill_stars_skill_agent_idx": { + "name": "company_skill_stars_skill_agent_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_stars_skill_user_idx": { + "name": "company_skill_stars_skill_user_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_stars_company_skill_created_idx": { + "name": "company_skill_stars_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_stars_company_id_companies_id_fk": { + "name": "company_skill_stars_company_id_companies_id_fk", + "tableFrom": "company_skill_stars", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_stars_company_skill_id_company_skills_id_fk": { + "name": "company_skill_stars_company_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_stars", + "tableTo": "company_skills", + "columnsFrom": [ + "company_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_stars_agent_id_agents_id_fk": { + "name": "company_skill_stars_agent_id_agents_id_fk", + "tableFrom": "company_skill_stars", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_test_inputs": { + "name": "company_skill_test_inputs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_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 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "company_skill_test_inputs_company_skill_name_idx": { + "name": "company_skill_test_inputs_company_skill_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_inputs_company_skill_active_idx": { + "name": "company_skill_test_inputs_company_skill_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_test_inputs_company_id_companies_id_fk": { + "name": "company_skill_test_inputs_company_id_companies_id_fk", + "tableFrom": "company_skill_test_inputs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_inputs_skill_id_company_skills_id_fk": { + "name": "company_skill_test_inputs_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_test_inputs", + "tableTo": "company_skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_test_run_templates": { + "name": "company_skill_test_run_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_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 + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "company_skill_test_run_templates_company_active_idx": { + "name": "company_skill_test_run_templates_company_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_test_run_templates_company_id_companies_id_fk": { + "name": "company_skill_test_run_templates_company_id_companies_id_fk", + "tableFrom": "company_skill_test_run_templates", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_run_templates_created_by_agent_id_agents_id_fk": { + "name": "company_skill_test_run_templates_created_by_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_run_templates", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skill_test_run_templates_updated_by_agent_id_agents_id_fk": { + "name": "company_skill_test_run_templates_updated_by_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_run_templates", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_test_runs": { + "name": "company_skill_test_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "input_id": { + "name": "input_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "input_snapshot": { + "name": "input_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "skill_version_id": { + "name": "skill_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_config_snapshot": { + "name": "agent_config_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_name": { + "name": "template_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_body": { + "name": "template_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_template_body": { + "name": "rendered_template_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness_issue_description": { + "name": "harness_issue_description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "output_document_key": { + "name": "output_document_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'output'" + }, + "output_snapshot": { + "name": "output_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "harness_issue_expires_at": { + "name": "harness_issue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "harness_issue_deleted_at": { + "name": "harness_issue_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": { + "company_skill_test_runs_company_skill_created_idx": { + "name": "company_skill_test_runs_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_issue_idx": { + "name": "company_skill_test_runs_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_input_created_idx": { + "name": "company_skill_test_runs_company_input_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "input_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_status_idx": { + "name": "company_skill_test_runs_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_test_runs_company_harness_expires_idx": { + "name": "company_skill_test_runs_company_harness_expires_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "harness_issue_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_test_runs_company_id_companies_id_fk": { + "name": "company_skill_test_runs_company_id_companies_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_runs_skill_id_company_skills_id_fk": { + "name": "company_skill_test_runs_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "company_skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_test_runs_input_id_company_skill_test_inputs_id_fk": { + "name": "company_skill_test_runs_input_id_company_skill_test_inputs_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "company_skill_test_inputs", + "columnsFrom": [ + "input_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skill_test_runs_skill_version_id_company_skill_versions_id_fk": { + "name": "company_skill_test_runs_skill_version_id_company_skill_versions_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "company_skill_versions", + "columnsFrom": [ + "skill_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "company_skill_test_runs_agent_id_agents_id_fk": { + "name": "company_skill_test_runs_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "company_skill_test_runs_issue_id_issues_id_fk": { + "name": "company_skill_test_runs_issue_id_issues_id_fk", + "tableFrom": "company_skill_test_runs", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skill_versions": { + "name": "company_skill_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_skill_id": { + "name": "company_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_name": { + "name": "release_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_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": { + "company_skill_versions_skill_revision_idx": { + "name": "company_skill_versions_skill_revision_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_versions_skill_release_idx": { + "name": "company_skill_versions_skill_release_idx", + "columns": [ + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"company_skill_versions\".\"release_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skill_versions_company_skill_created_idx": { + "name": "company_skill_versions_company_skill_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skill_versions_company_id_companies_id_fk": { + "name": "company_skill_versions_company_id_companies_id_fk", + "tableFrom": "company_skill_versions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_versions_company_skill_id_company_skills_id_fk": { + "name": "company_skill_versions_company_skill_id_company_skills_id_fk", + "tableFrom": "company_skill_versions", + "tableTo": "company_skills", + "columnsFrom": [ + "company_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_skill_versions_author_agent_id_agents_id_fk": { + "name": "company_skill_versions_author_agent_id_agents_id_fk", + "tableFrom": "company_skill_versions", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skills": { + "name": "company_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "source_locator": { + "name": "source_locator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_level": { + "name": "trust_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown_only'" + }, + "compatibility": { + "name": "compatibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compatible'" + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "homepage_url": { + "name": "homepage_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sharing_scope": { + "name": "sharing_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "public_share_token": { + "name": "public_share_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from_skill_id": { + "name": "forked_from_skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "forked_from_company_id": { + "name": "forked_from_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "star_count": { + "name": "star_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "install_count": { + "name": "install_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fork_count": { + "name": "fork_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "current_version_id": { + "name": "current_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skills_company_key_idx": { + "name": "company_skills_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_name_idx": { + "name": "company_skills_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_folder_idx": { + "name": "company_skills_company_folder_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_categories_idx": { + "name": "company_skills_company_categories_idx", + "columns": [ + { + "expression": "categories", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "company_skills_company_sharing_scope_idx": { + "name": "company_skills_company_sharing_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sharing_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_current_version_idx": { + "name": "company_skills_company_current_version_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_forked_from_idx": { + "name": "company_skills_company_forked_from_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "forked_from_skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_skills_folder_id_folders_id_fk": { + "name": "company_skills_folder_id_folders_id_fk", + "tableFrom": "company_skills", + "tableTo": "folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skills_forked_from_skill_id_company_skills_id_fk": { + "name": "company_skills_forked_from_skill_id_company_skills_id_fk", + "tableFrom": "company_skills", + "tableTo": "company_skills", + "columnsFrom": [ + "forked_from_skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skills_forked_from_company_id_companies_id_fk": { + "name": "company_skills_forked_from_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "forked_from_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "company_skills_current_version_id_company_skill_versions_id_fk": { + "name": "company_skills_current_version_id_company_skill_versions_id_fk", + "tableFrom": "company_skills", + "tableTo": "company_skill_versions", + "columnsFrom": [ + "current_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_transfer_runs": { + "name": "company_transfer_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "actor_key": { + "name": "actor_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_ref": { + "name": "container_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest_sha256": { + "name": "manifest_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "blob_count": { + "name": "blob_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completed_parts": { + "name": "completed_parts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_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": { + "company_transfer_runs_company_idx": { + "name": "company_transfer_runs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_transfer_runs_idempotency_direction_idx": { + "name": "company_transfer_runs_idempotency_direction_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_transfer_runs_actor_status_idx": { + "name": "company_transfer_runs_actor_status_idx", + "columns": [ + { + "expression": "actor_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_transfer_runs_company_id_companies_id_fk": { + "name": "company_transfer_runs_company_id_companies_id_fk", + "tableFrom": "company_transfer_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_user_sidebar_preferences": { + "name": "company_user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_order": { + "name": "project_order", + "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": { + "company_user_sidebar_preferences_company_idx": { + "name": "company_user_sidebar_preferences_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_user_idx": { + "name": "company_user_sidebar_preferences_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_company_user_uq": { + "name": "company_user_sidebar_preferences_company_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_user_sidebar_preferences_company_id_companies_id_fk": { + "name": "company_user_sidebar_preferences_company_id_companies_id_fk", + "tableFrom": "company_user_sidebar_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "cost_status": { + "name": "cost_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reported'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "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": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_archive_notification_outbox": { + "name": "decision_archive_notification_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archive_version": { + "name": "archive_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "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 + }, + "last_attempt_at": { + "name": "last_attempt_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": { + "decision_archive_notification_outbox_uq": { + "name": "decision_archive_notification_outbox_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archive_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_archive_notification_outbox_pending_idx": { + "name": "decision_archive_notification_outbox_pending_idx", + "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": { + "decision_archive_notification_outbox_company_id_companies_id_fk": { + "name": "decision_archive_notification_outbox_company_id_companies_id_fk", + "tableFrom": "decision_archive_notification_outbox", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_archive_notification_outbox_origin_agent_id_agents_id_fk": { + "name": "decision_archive_notification_outbox_origin_agent_id_agents_id_fk", + "tableFrom": "decision_archive_notification_outbox", + "tableTo": "agents", + "columnsFrom": [ + "origin_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_archive_notification_outbox_status_check": { + "name": "decision_archive_notification_outbox_status_check", + "value": "\"decision_archive_notification_outbox\".\"status\" IN ('pending', 'delivering', 'delivered')" + } + }, + "isRLSEnabled": false + }, + "public.decision_queue_items": { + "name": "decision_queue_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_type": { + "name": "added_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_agent_id": { + "name": "added_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by_run_id": { + "name": "added_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "added_by_agent_api_key_id": { + "name": "added_by_agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_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": { + "decision_queue_items_queue_source_uq": { + "name": "decision_queue_items_queue_source_uq", + "columns": [ + { + "expression": "queue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_queue_items_company_source_idx": { + "name": "decision_queue_items_company_source_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_queue_items_company_id_companies_id_fk": { + "name": "decision_queue_items_company_id_companies_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_queue_items_added_by_agent_id_agents_id_fk": { + "name": "decision_queue_items_added_by_agent_id_agents_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "agents", + "columnsFrom": [ + "added_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queue_items_added_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_queue_items_added_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "added_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queue_items_added_by_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_queue_items_added_by_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_queue_items", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "added_by_agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queue_items_queue_company_fk": { + "name": "decision_queue_items_queue_company_fk", + "tableFrom": "decision_queue_items", + "tableTo": "decision_queues", + "columnsFrom": [ + "queue_id", + "company_id" + ], + "columnsTo": [ + "id", + "company_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_queue_items_actor_check": { + "name": "decision_queue_items_actor_check", + "value": "(\n (\"decision_queue_items\".\"added_by_type\" = 'agent' AND \"decision_queue_items\".\"added_by_agent_id\" IS NOT NULL AND \"decision_queue_items\".\"added_by_user_id\" IS NULL)\n OR (\"decision_queue_items\".\"added_by_type\" = 'user' AND \"decision_queue_items\".\"added_by_agent_id\" IS NULL AND \"decision_queue_items\".\"added_by_user_id\" IS NOT NULL)\n OR (\"decision_queue_items\".\"added_by_type\" = 'system' AND \"decision_queue_items\".\"added_by_agent_id\" IS NULL AND \"decision_queue_items\".\"added_by_user_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_queues": { + "name": "decision_queues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_type": { + "name": "created_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_api_key_id": { + "name": "created_by_agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retention_days": { + "name": "retention_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seed_rules": { + "name": "seed_rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "seed_rules_enabled": { + "name": "seed_rules_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": { + "decision_queues_company_key_uq": { + "name": "decision_queues_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_queues_company_updated_idx": { + "name": "decision_queues_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_queues_company_id_companies_id_fk": { + "name": "decision_queues_company_id_companies_id_fk", + "tableFrom": "decision_queues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_queues_created_by_agent_id_agents_id_fk": { + "name": "decision_queues_created_by_agent_id_agents_id_fk", + "tableFrom": "decision_queues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queues_created_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_queues_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_queues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_queues_created_by_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_queues_created_by_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_queues", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "created_by_agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "decision_queues_id_company_uq": { + "name": "decision_queues_id_company_uq", + "nullsNotDistinct": false, + "columns": [ + "id", + "company_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "decision_queues_creator_check": { + "name": "decision_queues_creator_check", + "value": "(\n (\"decision_queues\".\"created_by_type\" = 'agent' AND \"decision_queues\".\"created_by_agent_id\" IS NOT NULL AND \"decision_queues\".\"created_by_user_id\" IS NULL)\n OR (\"decision_queues\".\"created_by_type\" = 'user' AND \"decision_queues\".\"created_by_agent_id\" IS NULL AND \"decision_queues\".\"created_by_user_id\" IS NOT NULL)\n OR (\"decision_queues\".\"created_by_type\" = 'system' AND \"decision_queues\".\"created_by_agent_id\" IS NULL AND \"decision_queues\".\"created_by_user_id\" IS NULL)\n )" + }, + "decision_queues_retention_days_check": { + "name": "decision_queues_retention_days_check", + "value": "\"decision_queues\".\"retention_days\" IS NULL OR (\"decision_queues\".\"retention_days\" >= 1 AND \"decision_queues\".\"retention_days\" <= 3650)" + } + }, + "isRLSEnabled": false + }, + "public.decision_retention": { + "name": "decision_retention", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_activity_at": { + "name": "source_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_reason": { + "name": "archived_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_type": { + "name": "archived_by_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_agent_id": { + "name": "archived_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_by_user_id": { + "name": "archived_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_run_id": { + "name": "archived_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "archive_version": { + "name": "archive_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": { + "decision_retention_company_source_uq": { + "name": "decision_retention_company_source_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_retention_company_archived_idx": { + "name": "decision_retention_company_archived_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_retention_company_id_companies_id_fk": { + "name": "decision_retention_company_id_companies_id_fk", + "tableFrom": "decision_retention", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_retention_archived_by_agent_id_agents_id_fk": { + "name": "decision_retention_archived_by_agent_id_agents_id_fk", + "tableFrom": "decision_retention", + "tableTo": "agents", + "columnsFrom": [ + "archived_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_retention_archived_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_retention_archived_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_retention", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "archived_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_retention_archive_actor_check": { + "name": "decision_retention_archive_actor_check", + "value": "(\n (\"decision_retention\".\"archived_at\" IS NULL AND \"decision_retention\".\"archived_by_type\" IS NULL AND \"decision_retention\".\"archived_by_agent_id\" IS NULL AND \"decision_retention\".\"archived_by_user_id\" IS NULL)\n OR (\"decision_retention\".\"archived_at\" IS NOT NULL AND \"decision_retention\".\"archived_by_type\" = 'system' AND \"decision_retention\".\"archived_by_agent_id\" IS NULL AND \"decision_retention\".\"archived_by_user_id\" IS NULL)\n OR (\"decision_retention\".\"archived_at\" IS NOT NULL AND \"decision_retention\".\"archived_by_type\" = 'agent' AND \"decision_retention\".\"archived_by_agent_id\" IS NOT NULL AND \"decision_retention\".\"archived_by_user_id\" IS NULL)\n OR (\"decision_retention\".\"archived_at\" IS NOT NULL AND \"decision_retention\".\"archived_by_type\" = 'user' AND \"decision_retention\".\"archived_by_agent_id\" IS NULL AND \"decision_retention\".\"archived_by_user_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_triage": { + "name": "decision_triage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decide_by": { + "name": "decide_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decide_by_date": { + "name": "decide_by_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "set_by_type": { + "name": "set_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "set_by_agent_id": { + "name": "set_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "set_by_user_id": { + "name": "set_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "set_by_run_id": { + "name": "set_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "set_by_agent_api_key_id": { + "name": "set_by_agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_triage_company_source_uq": { + "name": "decision_triage_company_source_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_triage_company_decide_by_idx": { + "name": "decision_triage_company_decide_by_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decide_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_triage_company_id_companies_id_fk": { + "name": "decision_triage_company_id_companies_id_fk", + "tableFrom": "decision_triage", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_triage_set_by_agent_id_agents_id_fk": { + "name": "decision_triage_set_by_agent_id_agents_id_fk", + "tableFrom": "decision_triage", + "tableTo": "agents", + "columnsFrom": [ + "set_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_set_by_run_id_heartbeat_runs_id_fk": { + "name": "decision_triage_set_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_triage", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "set_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_set_by_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_triage_set_by_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_triage", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "set_by_agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_triage_actor_check": { + "name": "decision_triage_actor_check", + "value": "(\n (\"decision_triage\".\"set_by_type\" = 'agent' AND \"decision_triage\".\"set_by_agent_id\" IS NOT NULL AND \"decision_triage\".\"set_by_user_id\" IS NULL)\n OR (\"decision_triage\".\"set_by_type\" = 'user' AND \"decision_triage\".\"set_by_agent_id\" IS NULL AND \"decision_triage\".\"set_by_user_id\" IS NOT NULL)\n )" + }, + "decision_triage_decide_by_check": { + "name": "decision_triage_decide_by_check", + "value": "(\n (\"decision_triage\".\"decide_by\" IS NULL AND \"decision_triage\".\"decide_by_date\" IS NULL)\n OR (\"decision_triage\".\"decide_by\" IN ('today', 'this_week', 'whenever') AND \"decision_triage\".\"decide_by_date\" IS NULL)\n OR (\"decision_triage\".\"decide_by\" = 'date' AND \"decision_triage\".\"decide_by_date\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_triage_events": { + "name": "decision_triage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_id": { + "name": "queue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_run_id": { + "name": "actor_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_api_key_id": { + "name": "agent_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "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": { + "decision_triage_events_company_source_created_idx": { + "name": "decision_triage_events_company_source_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_triage_events_queue_created_idx": { + "name": "decision_triage_events_queue_created_idx", + "columns": [ + { + "expression": "queue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_triage_events_company_id_companies_id_fk": { + "name": "decision_triage_events_company_id_companies_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_triage_events_queue_id_decision_queues_id_fk": { + "name": "decision_triage_events_queue_id_decision_queues_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "decision_queues", + "columnsFrom": [ + "queue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_events_actor_agent_id_agents_id_fk": { + "name": "decision_triage_events_actor_agent_id_agents_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_events_actor_run_id_heartbeat_runs_id_fk": { + "name": "decision_triage_events_actor_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "actor_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_triage_events_agent_api_key_id_agent_api_keys_id_fk": { + "name": "decision_triage_events_agent_api_key_id_agent_api_keys_id_fk", + "tableFrom": "decision_triage_events", + "tableTo": "agent_api_keys", + "columnsFrom": [ + "agent_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "decision_triage_events_actor_check": { + "name": "decision_triage_events_actor_check", + "value": "(\n (\"decision_triage_events\".\"actor_type\" = 'agent' AND \"decision_triage_events\".\"actor_agent_id\" IS NOT NULL AND \"decision_triage_events\".\"actor_user_id\" IS NULL)\n OR (\"decision_triage_events\".\"actor_type\" = 'user' AND \"decision_triage_events\".\"actor_agent_id\" IS NULL AND \"decision_triage_events\".\"actor_user_id\" IS NOT NULL)\n OR (\"decision_triage_events\".\"actor_type\" = 'system' AND \"decision_triage_events\".\"actor_agent_id\" IS NULL AND \"decision_triage_events\".\"actor_user_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.decision_training_examples": { + "name": "decision_training_examples", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cutoff_at": { + "name": "cutoff_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "notes_history": { + "name": "notes_history", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "decision_outcome": { + "name": "decision_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scrub_deleted_comments_v1'" + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_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": { + "decision_training_examples_company_created_at_idx": { + "name": "decision_training_examples_company_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_training_examples_issue_idx": { + "name": "decision_training_examples_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_training_examples_source_author_uq": { + "name": "decision_training_examples_source_author_uq", + "columns": [ + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_training_examples_company_id_companies_id_fk": { + "name": "decision_training_examples_company_id_companies_id_fk", + "tableFrom": "decision_training_examples", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_training_examples_issue_id_issues_id_fk": { + "name": "decision_training_examples_issue_id_issues_id_fk", + "tableFrom": "decision_training_examples", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_bundles": { + "name": "decision_bundles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decision_bundles_company_created_at_idx": { + "name": "decision_bundles_company_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_bundles_company_id_companies_id_fk": { + "name": "decision_bundles_company_id_companies_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_bundles_origin_agent_id_agents_id_fk": { + "name": "decision_bundles_origin_agent_id_agents_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "agents", + "columnsFrom": [ + "origin_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_bundles_origin_issue_id_issues_id_fk": { + "name": "decision_bundles_origin_issue_id_issues_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "issues", + "columnsFrom": [ + "origin_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_bundles_origin_run_id_heartbeat_runs_id_fk": { + "name": "decision_bundles_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_bundles", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "origin_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_effect_executions": { + "name": "decision_effect_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effect_index": { + "name": "effect_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "effect_type": { + "name": "effect_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_issue_id": { + "name": "target_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claimed'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activity_log_id": { + "name": "activity_log_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "decision_effect_executions_decision_effect_uq": { + "name": "decision_effect_executions_decision_effect_uq", + "columns": [ + { + "expression": "decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effect_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_effect_executions_target_issue_idx": { + "name": "decision_effect_executions_target_issue_idx", + "columns": [ + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_effect_executions_decision_id_decisions_id_fk": { + "name": "decision_effect_executions_decision_id_decisions_id_fk", + "tableFrom": "decision_effect_executions", + "tableTo": "decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_effect_executions_target_issue_id_issues_id_fk": { + "name": "decision_effect_executions_target_issue_id_issues_id_fk", + "tableFrom": "decision_effect_executions", + "tableTo": "issues", + "columnsFrom": [ + "target_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decision_effect_executions_activity_log_id_activity_log_id_fk": { + "name": "decision_effect_executions_activity_log_id_activity_log_id_fk", + "tableFrom": "decision_effect_executions", + "tableTo": "activity_log", + "columnsFrom": [ + "activity_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decision_target_issues": { + "name": "decision_target_issues", + "schema": "", + "columns": { + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "decision_target_issues_decision_idx": { + "name": "decision_target_issues_decision_idx", + "columns": [ + { + "expression": "decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decision_target_issues_issue_idx": { + "name": "decision_target_issues_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decision_target_issues_decision_id_decisions_id_fk": { + "name": "decision_target_issues_decision_id_decisions_id_fk", + "tableFrom": "decision_target_issues", + "tableTo": "decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_target_issues_issue_id_issues_id_fk": { + "name": "decision_target_issues_issue_id_issues_id_fk", + "tableFrom": "decision_target_issues", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "decision_target_issues_company_id_companies_id_fk": { + "name": "decision_target_issues_company_id_companies_id_fk", + "tableFrom": "decision_target_issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "decision_target_issues_decision_id_issue_id_pk": { + "name": "decision_target_issues_decision_id_issue_id_pk", + "columns": [ + "decision_id", + "issue_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decisions": { + "name": "decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bundle_id": { + "name": "bundle_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin_agent_id": { + "name": "origin_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_issue_id": { + "name": "origin_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rule_key": { + "name": "rule_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "execution_status": { + "name": "execution_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chosen_option_id": { + "name": "chosen_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_values": { + "name": "input_values", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signed_spec": { + "name": "signed_spec", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_snapshots": { + "name": "target_snapshots", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "continuation_policy": { + "name": "continuation_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "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": { + "decisions_company_status_expires_at_idx": { + "name": "decisions_company_status_expires_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_bundle_idx": { + "name": "decisions_bundle_idx", + "columns": [ + { + "expression": "bundle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_origin_issue_idx": { + "name": "decisions_origin_issue_idx", + "columns": [ + { + "expression": "origin_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "decisions_company_idempotency_uq": { + "name": "decisions_company_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"decisions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decisions_company_id_companies_id_fk": { + "name": "decisions_company_id_companies_id_fk", + "tableFrom": "decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_bundle_id_decision_bundles_id_fk": { + "name": "decisions_bundle_id_decision_bundles_id_fk", + "tableFrom": "decisions", + "tableTo": "decision_bundles", + "columnsFrom": [ + "bundle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "decisions_origin_agent_id_agents_id_fk": { + "name": "decisions_origin_agent_id_agents_id_fk", + "tableFrom": "decisions", + "tableTo": "agents", + "columnsFrom": [ + "origin_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_origin_issue_id_issues_id_fk": { + "name": "decisions_origin_issue_id_issues_id_fk", + "tableFrom": "decisions", + "tableTo": "issues", + "columnsFrom": [ + "origin_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_origin_run_id_heartbeat_runs_id_fk": { + "name": "decisions_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "origin_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_annotation_anchor_snapshots": { + "name": "document_annotation_anchor_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_revision_id": { + "name": "from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "from_revision_number": { + "name": "from_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "to_revision_id": { + "name": "to_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_revision_number": { + "name": "to_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_anchor": { + "name": "previous_anchor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "next_anchor": { + "name": "next_anchor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "anchor_state": { + "name": "anchor_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor_confidence": { + "name": "anchor_confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_annotation_anchor_snapshots_company_thread_created_at_idx": { + "name": "document_annotation_anchor_snapshots_company_thread_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_anchor_snapshots_company_document_revision_idx": { + "name": "document_annotation_anchor_snapshots_company_document_revision_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_annotation_anchor_snapshots_company_id_companies_id_fk": { + "name": "document_annotation_anchor_snapshots_company_id_companies_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_thread_id_document_annotation_threads_id_fk": { + "name": "document_annotation_anchor_snapshots_thread_id_document_annotation_threads_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "document_annotation_threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_document_id_documents_id_fk": { + "name": "document_annotation_anchor_snapshots_document_id_documents_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_from_revision_id_document_revisions_id_fk": { + "name": "document_annotation_anchor_snapshots_from_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "document_revisions", + "columnsFrom": [ + "from_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_anchor_snapshots_to_revision_id_document_revisions_id_fk": { + "name": "document_annotation_anchor_snapshots_to_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "tableTo": "document_revisions", + "columnsFrom": [ + "to_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_annotation_comments": { + "name": "document_annotation_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_type": { + "name": "author_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "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": { + "document_annotation_comments_company_thread_created_at_idx": { + "name": "document_annotation_comments_company_thread_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_issue_created_at_idx": { + "name": "document_annotation_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_routine_created_at_idx": { + "name": "document_annotation_comments_company_routine_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_case_created_at_idx": { + "name": "document_annotation_comments_company_case_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_company_document_created_at_idx": { + "name": "document_annotation_comments_company_document_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_issue_comment_idx": { + "name": "document_annotation_comments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_comments_body_search_idx": { + "name": "document_annotation_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "document_annotation_comments_company_id_companies_id_fk": { + "name": "document_annotation_comments_company_id_companies_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_annotation_comments_thread_id_document_annotation_threads_id_fk": { + "name": "document_annotation_comments_thread_id_document_annotation_threads_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "document_annotation_threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_issue_id_issues_id_fk": { + "name": "document_annotation_comments_issue_id_issues_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_routine_id_routines_id_fk": { + "name": "document_annotation_comments_routine_id_routines_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_case_id_cases_id_fk": { + "name": "document_annotation_comments_case_id_cases_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_document_id_documents_id_fk": { + "name": "document_annotation_comments_document_id_documents_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_comments_author_agent_id_agents_id_fk": { + "name": "document_annotation_comments_author_agent_id_agents_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_annotation_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_comments_issue_comment_id_issue_comments_id_fk": { + "name": "document_annotation_comments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "document_annotation_comments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_annotation_comments_exactly_one_owner_chk": { + "name": "document_annotation_comments_exactly_one_owner_chk", + "value": "num_nonnulls(\"document_annotation_comments\".\"issue_id\", \"document_annotation_comments\".\"routine_id\", \"document_annotation_comments\".\"case_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.document_annotation_threads": { + "name": "document_annotation_threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "anchor_state": { + "name": "anchor_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "original_revision_id": { + "name": "original_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_revision_number": { + "name": "original_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_revision_id": { + "name": "current_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "current_revision_number": { + "name": "current_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "selected_text": { + "name": "selected_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix_text": { + "name": "prefix_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "suffix_text": { + "name": "suffix_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "normalized_start": { + "name": "normalized_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "normalized_end": { + "name": "normalized_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "markdown_start": { + "name": "markdown_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "markdown_end": { + "name": "markdown_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "anchor_confidence": { + "name": "anchor_confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exact'" + }, + "anchor_selector": { + "name": "anchor_selector", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "document_annotation_threads_company_document_status_idx": { + "name": "document_annotation_threads_company_document_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_issue_status_idx": { + "name": "document_annotation_threads_company_issue_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_routine_status_idx": { + "name": "document_annotation_threads_company_routine_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_case_status_idx": { + "name": "document_annotation_threads_company_case_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_current_revision_open_idx": { + "name": "document_annotation_threads_company_current_revision_open_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_annotation_threads_company_anchor_state_idx": { + "name": "document_annotation_threads_company_anchor_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "anchor_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_annotation_threads_company_id_companies_id_fk": { + "name": "document_annotation_threads_company_id_companies_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_annotation_threads_issue_id_issues_id_fk": { + "name": "document_annotation_threads_issue_id_issues_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_routine_id_routines_id_fk": { + "name": "document_annotation_threads_routine_id_routines_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_case_id_cases_id_fk": { + "name": "document_annotation_threads_case_id_cases_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_document_id_documents_id_fk": { + "name": "document_annotation_threads_document_id_documents_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_annotation_threads_original_revision_id_document_revisions_id_fk": { + "name": "document_annotation_threads_original_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "document_revisions", + "columnsFrom": [ + "original_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_threads_current_revision_id_document_revisions_id_fk": { + "name": "document_annotation_threads_current_revision_id_document_revisions_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "document_revisions", + "columnsFrom": [ + "current_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_threads_created_by_agent_id_agents_id_fk": { + "name": "document_annotation_threads_created_by_agent_id_agents_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_annotation_threads_resolved_by_agent_id_agents_id_fk": { + "name": "document_annotation_threads_resolved_by_agent_id_agents_id_fk", + "tableFrom": "document_annotation_threads", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_annotation_threads_exactly_one_owner_chk": { + "name": "document_annotation_threads_exactly_one_owner_chk", + "value": "num_nonnulls(\"document_annotation_threads\".\"issue_id\", \"document_annotation_threads\".\"routine_id\", \"document_annotation_threads\".\"case_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.document_memberships": { + "name": "document_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_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": { + "document_memberships_company_user_starred_idx": { + "name": "document_memberships_company_user_starred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_memberships_company_user_document_uq": { + "name": "document_memberships_company_user_document_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_memberships_company_id_companies_id_fk": { + "name": "document_memberships_company_id_companies_id_fk", + "tableFrom": "document_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_memberships_document_id_documents_id_fk": { + "name": "document_memberships_document_id_documents_id_fk", + "tableFrom": "document_memberships", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by_agent_id": { + "name": "locked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "locked_by_user_id": { + "name": "locked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "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": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_title_search_idx": { + "name": "documents_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "documents_latest_body_search_idx": { + "name": "documents_latest_body_search_idx", + "columns": [ + { + "expression": "latest_body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_locked_by_agent_id_agents_id_fk": { + "name": "documents_locked_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "locked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_custom_image_setup_sessions": { + "name": "environment_custom_image_setup_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "promoted_template_id": { + "name": "promoted_template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_lease_id": { + "name": "environment_lease_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'starting'" + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_by_agent_id": { + "name": "started_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "base_template_ref": { + "name": "base_template_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_summary": { + "name": "connection_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "connection_secret_ref": { + "name": "connection_secret_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_custom_image_setup_sessions_environment_status_idx": { + "name": "environment_custom_image_setup_sessions_environment_status_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_environment_active_uq": { + "name": "environment_custom_image_setup_sessions_environment_active_uq", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environment_custom_image_setup_sessions\".\"status\" IN ('starting', 'waiting_for_user', 'capturing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_template_idx": { + "name": "environment_custom_image_setup_sessions_template_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_promoted_template_idx": { + "name": "environment_custom_image_setup_sessions_promoted_template_idx", + "columns": [ + { + "expression": "promoted_template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_expires_idx": { + "name": "environment_custom_image_setup_sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_setup_sessions_provider_lease_idx": { + "name": "environment_custom_image_setup_sessions_provider_lease_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_custom_image_setup_sessions_environment_id_environments_id_fk": { + "name": "environment_custom_image_setup_sessions_environment_id_environments_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_template_id_environment_custom_image_templates_id_fk": { + "name": "environment_custom_image_setup_sessions_template_id_environment_custom_image_templates_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environment_custom_image_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_promoted_template_id_environment_custom_image_templates_id_fk": { + "name": "environment_custom_image_setup_sessions_promoted_template_id_environment_custom_image_templates_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environment_custom_image_templates", + "columnsFrom": [ + "promoted_template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_environment_lease_id_environment_leases_id_fk": { + "name": "environment_custom_image_setup_sessions_environment_lease_id_environment_leases_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "environment_leases", + "columnsFrom": [ + "environment_lease_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_setup_sessions_started_by_agent_id_agents_id_fk": { + "name": "environment_custom_image_setup_sessions_started_by_agent_id_agents_id_fk", + "tableFrom": "environment_custom_image_setup_sessions", + "tableTo": "agents", + "columnsFrom": [ + "started_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_custom_image_templates": { + "name": "environment_custom_image_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_kind": { + "name": "template_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "template_ref": { + "name": "template_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_template_ref": { + "name": "source_template_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_environment_config_fingerprint": { + "name": "source_environment_config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "captured_at": { + "name": "captured_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 + }, + "superseded_by_template_id": { + "name": "superseded_by_template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_custom_image_templates_environment_status_idx": { + "name": "environment_custom_image_templates_environment_status_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_environment_provider_status_idx": { + "name": "environment_custom_image_templates_environment_provider_status_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_environment_active_uq": { + "name": "environment_custom_image_templates_environment_active_uq", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environment_custom_image_templates\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_superseded_by_idx": { + "name": "environment_custom_image_templates_superseded_by_idx", + "columns": [ + { + "expression": "superseded_by_template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_custom_image_templates_last_used_idx": { + "name": "environment_custom_image_templates_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_custom_image_templates_environment_id_environments_id_fk": { + "name": "environment_custom_image_templates_environment_id_environments_id_fk", + "tableFrom": "environment_custom_image_templates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_custom_image_templates_created_by_agent_id_agents_id_fk": { + "name": "environment_custom_image_templates_created_by_agent_id_agents_id_fk", + "tableFrom": "environment_custom_image_templates", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_custom_image_templates_superseded_by_template_id_environment_custom_image_templates_id_fk": { + "name": "environment_custom_image_templates_superseded_by_template_id_environment_custom_image_templates_id_fk", + "tableFrom": "environment_custom_image_templates", + "tableTo": "environment_custom_image_templates", + "columnsFrom": [ + "superseded_by_template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_leases": { + "name": "environment_leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "lease_policy": { + "name": "lease_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ephemeral'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acquired_at": { + "name": "acquired_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": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_status": { + "name": "cleanup_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_leases_company_environment_status_idx": { + "name": "environment_leases_company_environment_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_execution_workspace_idx": { + "name": "environment_leases_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_issue_idx": { + "name": "environment_leases_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_heartbeat_run_idx": { + "name": "environment_leases_heartbeat_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_last_used_idx": { + "name": "environment_leases_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_provider_lease_idx": { + "name": "environment_leases_provider_lease_idx", + "columns": [ + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_leases_company_id_companies_id_fk": { + "name": "environment_leases_company_id_companies_id_fk", + "tableFrom": "environment_leases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_leases_environment_id_environments_id_fk": { + "name": "environment_leases_environment_id_environments_id_fk", + "tableFrom": "environment_leases", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_execution_workspace_id_execution_workspaces_id_fk": { + "name": "environment_leases_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "environment_leases", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_issue_id_issues_id_fk": { + "name": "environment_leases_issue_id_issues_id_fk", + "tableFrom": "environment_leases", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "environment_leases", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver": { + "name": "driver", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environments_status_idx": { + "name": "environments_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_local_driver_idx": { + "name": "environments_local_driver_idx", + "columns": [ + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environments\".\"driver\" = 'local'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_managed_sandbox_idx": { + "name": "environments_managed_sandbox_idx", + "columns": [ + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environments\".\"driver\" = 'sandbox' AND (\"environments\".\"metadata\" ->> 'managedByPaperclip')::boolean = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_name_idx": { + "name": "environments_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspace_runtime_leases": { + "name": "execution_workspace_runtime_leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_key": { + "name": "owner_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_issue_id": { + "name": "owner_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_run_id": { + "name": "owner_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_action": { + "name": "last_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "renewed_at": { + "name": "renewed_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 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspace_runtime_leases_company_workspace_idx": { + "name": "execution_workspace_runtime_leases_company_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_runtime_leases_company_owner_idx": { + "name": "execution_workspace_runtime_leases_company_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspace_runtime_leases_expires_at_idx": { + "name": "execution_workspace_runtime_leases_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspace_runtime_leases_company_id_companies_id_fk": { + "name": "execution_workspace_runtime_leases_company_id_companies_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspace_runtime_leases_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_owner_issue_id_issues_id_fk": { + "name": "execution_workspace_runtime_leases_owner_issue_id_issues_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "issues", + "columnsFrom": [ + "owner_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_owner_run_id_heartbeat_runs_id_fk": { + "name": "execution_workspace_runtime_leases_owner_run_id_heartbeat_runs_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "owner_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspace_runtime_leases_owner_agent_id_agents_id_fk": { + "name": "execution_workspace_runtime_leases_owner_agent_id_agents_id_fk", + "tableFrom": "execution_workspace_runtime_leases", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "execution_workspace_runtime_leases_execution_workspace_id_unique": { + "name": "execution_workspace_runtime_leases_execution_workspace_id_unique", + "nullsNotDistinct": false, + "columns": [ + "execution_workspace_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspaces": { + "name": "execution_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy_type": { + "name": "strategy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_fs'" + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "derived_from_execution_workspace_id": { + "name": "derived_from_execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_eligible_at": { + "name": "cleanup_eligible_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_reason": { + "name": "cleanup_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspaces_company_project_status_idx": { + "name": "execution_workspaces_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_project_workspace_status_idx": { + "name": "execution_workspaces_company_project_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_source_issue_idx": { + "name": "execution_workspaces_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_last_used_idx": { + "name": "execution_workspaces_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_branch_idx": { + "name": "execution_workspaces_company_branch_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspaces_company_id_companies_id_fk": { + "name": "execution_workspaces_company_id_companies_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_id_projects_id_fk": { + "name": "execution_workspaces_project_id_projects_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_workspace_id_project_workspaces_id_fk": { + "name": "execution_workspaces_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_source_issue_id_issues_id_fk": { + "name": "execution_workspaces_source_issue_id_issues_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "derived_from_execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_object_mentions": { + "name": "external_object_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_record_id": { + "name": "source_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "property_key": { + "name": "property_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_text_redacted": { + "name": "matched_text_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sanitized_display_url": { + "name": "sanitized_display_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_identity_hash": { + "name": "canonical_identity_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_identity": { + "name": "canonical_identity", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detector_key": { + "name": "detector_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "object_type": { + "name": "object_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exact'" + }, + "created_by_plugin_id": { + "name": "created_by_plugin_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": { + "external_object_mentions_company_source_issue_idx": { + "name": "external_object_mentions_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_object_idx": { + "name": "external_object_mentions_company_object_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_provider_idx": { + "name": "external_object_mentions_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_source_record_uq": { + "name": "external_object_mentions_company_source_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_identity_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"external_object_mentions\".\"source_record_id\" is not null and \"external_object_mentions\".\"canonical_identity_hash\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_object_mentions_company_source_null_record_uq": { + "name": "external_object_mentions_company_source_null_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_identity_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"external_object_mentions\".\"source_record_id\" is null and \"external_object_mentions\".\"canonical_identity_hash\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_object_mentions_company_id_companies_id_fk": { + "name": "external_object_mentions_company_id_companies_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_object_mentions_source_issue_id_issues_id_fk": { + "name": "external_object_mentions_source_issue_id_issues_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_object_mentions_object_id_external_objects_id_fk": { + "name": "external_object_mentions_object_id_external_objects_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "external_objects", + "columnsFrom": [ + "object_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_object_mentions_created_by_plugin_id_plugins_id_fk": { + "name": "external_object_mentions_created_by_plugin_id_plugins_id_fk", + "tableFrom": "external_object_mentions", + "tableTo": "plugins", + "columnsFrom": [ + "created_by_plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_objects": { + "name": "external_objects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_key": { + "name": "provider_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "object_type": { + "name": "object_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sanitized_canonical_url": { + "name": "sanitized_canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_identity_hash": { + "name": "canonical_identity_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_key": { + "name": "display_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_key": { + "name": "icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_title": { + "name": "display_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_key": { + "name": "status_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_label": { + "name": "status_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_icon_key": { + "name": "status_icon_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_category": { + "name": "status_category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "status_tone": { + "name": "status_tone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'neutral'" + }, + "liveness": { + "name": "liveness", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "is_terminal": { + "name": "is_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "remote_version": { + "name": "remote_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_resolved_at": { + "name": "last_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_changed_at": { + "name": "last_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_refresh_at": { + "name": "next_refresh_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_started_at": { + "name": "refresh_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "uuid", + "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": { + "external_objects_company_provider_object_idx": { + "name": "external_objects_company_provider_object_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_provider_status_idx": { + "name": "external_objects_company_provider_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_refresh_idx": { + "name": "external_objects_company_refresh_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_refresh_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_external_id_uq": { + "name": "external_objects_company_external_id_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "external_objects_company_identity_uq": { + "name": "external_objects_company_identity_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_identity_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "external_objects_company_id_companies_id_fk": { + "name": "external_objects_company_id_companies_id_fk", + "tableFrom": "external_objects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_objects_plugin_id_plugins_id_fk": { + "name": "external_objects_plugin_id_plugins_id_fk", + "tableFrom": "external_objects", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_exports": { + "name": "feedback_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "feedback_vote_id": { + "name": "feedback_vote_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "export_id": { + "name": "export_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-envelope-v2'" + }, + "bundle_version": { + "name": "bundle_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-bundle-v2'" + }, + "payload_version": { + "name": "payload_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-v1'" + }, + "payload_digest": { + "name": "payload_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_snapshot": { + "name": "payload_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_summary": { + "name": "target_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "exported_at": { + "name": "exported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_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": { + "feedback_exports_feedback_vote_idx": { + "name": "feedback_exports_feedback_vote_idx", + "columns": [ + { + "expression": "feedback_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_created_idx": { + "name": "feedback_exports_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_status_idx": { + "name": "feedback_exports_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_issue_idx": { + "name": "feedback_exports_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_project_idx": { + "name": "feedback_exports_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_author_idx": { + "name": "feedback_exports_company_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_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": { + "feedback_exports_company_id_companies_id_fk": { + "name": "feedback_exports_company_id_companies_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_exports_feedback_vote_id_feedback_votes_id_fk": { + "name": "feedback_exports_feedback_vote_id_feedback_votes_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "feedback_votes", + "columnsFrom": [ + "feedback_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_issue_id_issues_id_fk": { + "name": "feedback_exports_issue_id_issues_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_project_id_projects_id_fk": { + "name": "feedback_exports_project_id_projects_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_votes": { + "name": "feedback_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_with_labs": { + "name": "shared_with_labs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redaction_summary": { + "name": "redaction_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": { + "feedback_votes_company_issue_idx": { + "name": "feedback_votes_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_issue_target_idx": { + "name": "feedback_votes_issue_target_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_author_idx": { + "name": "feedback_votes_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_company_target_author_idx": { + "name": "feedback_votes_company_target_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_votes_company_id_companies_id_fk": { + "name": "feedback_votes_company_id_companies_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_votes_issue_id_issues_id_fk": { + "name": "feedback_votes_issue_id_issues_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folders": { + "name": "folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "system_key": { + "name": "system_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "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": { + "folders_company_kind_position_idx": { + "name": "folders_company_kind_position_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_root_slug_uq": { + "name": "folders_company_kind_root_slug_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folders\".\"parent_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_parent_slug_uq": { + "name": "folders_company_kind_parent_slug_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folders\".\"parent_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_system_key_uq": { + "name": "folders_company_kind_system_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "system_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folders\".\"system_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folders_company_kind_parent_position_idx": { + "name": "folders_company_kind_parent_position_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folders_company_id_companies_id_fk": { + "name": "folders_company_id_companies_id_fk", + "tableFrom": "folders", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folders_parent_id_folders_id_fk": { + "name": "folders_parent_id_folders_id_fk", + "tableFrom": "folders", + "tableTo": "folders", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_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": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_watchdog_decisions": { + "name": "heartbeat_run_watchdog_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluation_issue_id": { + "name": "evaluation_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_watchdog_decisions_company_run_created_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_watchdog_decisions_company_run_snooze_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_snooze_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snoozed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_watchdog_decisions_company_id_companies_id_fk": { + "name": "heartbeat_run_watchdog_decisions_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk": { + "name": "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "issues", + "columnsFrom": [ + "evaluation_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_pid": { + "name": "process_pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_group_id": { + "name": "process_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_started_at": { + "name": "process_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_at": { + "name": "last_output_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_seq": { + "name": "last_output_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_output_stream": { + "name": "last_output_stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_output_bytes": { + "name": "last_output_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "retry_of_run_id": { + "name": "retry_of_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "process_loss_retry_count": { + "name": "process_loss_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_at": { + "name": "scheduled_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scheduled_retry_attempt": { + "name": "scheduled_retry_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_reason": { + "name": "scheduled_retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_comment_status": { + "name": "issue_comment_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_applicable'" + }, + "issue_comment_satisfied_by_comment_id": { + "name": "issue_comment_satisfied_by_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_comment_retry_queued_at": { + "name": "issue_comment_retry_queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "liveness_state": { + "name": "liveness_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "liveness_reason": { + "name": "liveness_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "continuation_attempt": { + "name": "continuation_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_useful_action_at": { + "name": "last_useful_action_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_snapshot": { + "name": "context_snapshot", + "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": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_responsible_user_idx": { + "name": "heartbeat_runs_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_liveness_idx": { + "name": "heartbeat_runs_company_liveness_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "liveness_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_last_output_idx": { + "name": "heartbeat_runs_company_status_last_output_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_output_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_process_started_idx": { + "name": "heartbeat_runs_company_status_process_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "process_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_created_at_desc_idx": { + "name": "heartbeat_runs_company_created_at_desc_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_ctx_issue_created_idx": { + "name": "heartbeat_runs_company_ctx_issue_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"context_snapshot\" ->> 'issueId')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_ctx_task_created_idx": { + "name": "heartbeat_runs_company_ctx_task_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"context_snapshot\" ->> 'taskId')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_ctx_taskkey_created_idx": { + "name": "heartbeat_runs_company_ctx_taskkey_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "(\"context_snapshot\" ->> 'taskKey')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "retry_of_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_dismissals": { + "name": "inbox_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "name": "item_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dismiss'" + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "snoozed_until": { + "name": "snoozed_until", + "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": { + "inbox_dismissals_company_user_idx": { + "name": "inbox_dismissals_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_item_idx": { + "name": "inbox_dismissals_company_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_user_item_idx": { + "name": "inbox_dismissals_company_user_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_dismissals_company_id_companies_id_fk": { + "name": "inbox_dismissals_company_id_companies_id_fk", + "tableFrom": "inbox_dismissals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_grants": { + "name": "connection_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant": { + "name": "provider_tenant", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_secret_refs": { + "name": "credential_secret_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_by_agent_id": { + "name": "revoked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revoked_by_user_id": { + "name": "revoked_by_user_id", + "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": { + "connection_grants_company_connection_idx": { + "name": "connection_grants_company_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_grants_subject_user_idx": { + "name": "connection_grants_subject_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_grants_user_uq": { + "name": "connection_grants_user_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_grants_default_uq": { + "name": "connection_grants_default_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"connection_grants\".\"is_default\" = true and \"connection_grants\".\"kind\" = 'workspace'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connection_grants_company_id_companies_id_fk": { + "name": "connection_grants_company_id_companies_id_fk", + "tableFrom": "connection_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_grants_created_by_agent_id_agents_id_fk": { + "name": "connection_grants_created_by_agent_id_agents_id_fk", + "tableFrom": "connection_grants", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_grants_revoked_by_agent_id_agents_id_fk": { + "name": "connection_grants_revoked_by_agent_id_agents_id_fk", + "tableFrom": "connection_grants", + "tableTo": "agents", + "columnsFrom": [ + "revoked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_grants_company_connection_fk": { + "name": "connection_grants_company_connection_fk", + "tableFrom": "connection_grants", + "tableTo": "tool_connections", + "columnsFrom": [ + "company_id", + "connection_id" + ], + "columnsTo": [ + "company_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "connection_grants_kind_check": { + "name": "connection_grants_kind_check", + "value": "\"connection_grants\".\"kind\" in ('workspace', 'user')" + }, + "connection_grants_status_check": { + "name": "connection_grants_status_check", + "value": "\"connection_grants\".\"status\" in ('active', 'revoked', 'expired', 'needs_reauthorization')" + }, + "connection_grants_subject_check": { + "name": "connection_grants_subject_check", + "value": "(\"connection_grants\".\"kind\" = 'user' and \"connection_grants\".\"subject_user_id\" is not null) or (\"connection_grants\".\"kind\" = 'workspace' and \"connection_grants\".\"subject_user_id\" is null)" + }, + "connection_grants_default_check": { + "name": "connection_grants_default_check", + "value": "\"connection_grants\".\"is_default\" = false or \"connection_grants\".\"kind\" = 'workspace'" + } + }, + "isRLSEnabled": false + }, + "public.connection_token_issuances": { + "name": "connection_token_issuances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_scope": { + "name": "requested_scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "issued_scope": { + "name": "issued_scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_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()" + } + }, + "indexes": { + "connection_token_issuances_company_created_idx": { + "name": "connection_token_issuances_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_token_issuances_connection_created_idx": { + "name": "connection_token_issuances_connection_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_token_issuances_agent_connection_idx": { + "name": "connection_token_issuances_agent_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_token_issuances_run_idx": { + "name": "connection_token_issuances_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connection_token_issuances_company_id_companies_id_fk": { + "name": "connection_token_issuances_company_id_companies_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_token_issuances_application_id_tool_applications_id_fk": { + "name": "connection_token_issuances_application_id_tool_applications_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_token_issuances_connection_id_tool_connections_id_fk": { + "name": "connection_token_issuances_connection_id_tool_connections_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_token_issuances_agent_id_agents_id_fk": { + "name": "connection_token_issuances_agent_id_agents_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "connection_token_issuances_run_id_heartbeat_runs_id_fk": { + "name": "connection_token_issuances_run_id_heartbeat_runs_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_token_issuances_issue_id_issues_id_fk": { + "name": "connection_token_issuances_issue_id_issues_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "connection_token_issuances_project_id_projects_id_fk": { + "name": "connection_token_issuances_project_id_projects_id_fk", + "tableFrom": "connection_token_issuances", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "singleton_key": { + "name": "singleton_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "experimental": { + "name": "experimental", + "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": { + "instance_settings_singleton_key_idx": { + "name": "instance_settings_singleton_key_idx", + "columns": [ + { + "expression": "singleton_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "instance_settings_default_environment_id_environments_id_fk": { + "name": "instance_settings_default_environment_id_environments_id_fk", + "tableFrom": "instance_settings", + "tableTo": "environments", + "columnsFrom": [ + "default_environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "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 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_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": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_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": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_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": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "on_behalf_of_user_id": { + "name": "on_behalf_of_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_type": { + "name": "author_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "derived_author_agent_id": { + "name": "derived_author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "derived_created_by_run_id": { + "name": "derived_created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "derived_author_source": { + "name": "derived_author_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "presentation": { + "name": "presentation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_by_type": { + "name": "deleted_by_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_by_agent_id": { + "name": "deleted_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deleted_by_user_id": { + "name": "deleted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_by_run_id": { + "name": "deleted_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "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": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_body_search_idx": { + "name": "issue_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_on_behalf_of_user_id_user_id_fk": { + "name": "issue_comments_on_behalf_of_user_id_user_id_fk", + "tableFrom": "issue_comments", + "tableTo": "user", + "columnsFrom": [ + "on_behalf_of_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_derived_author_agent_id_agents_id_fk": { + "name": "issue_comments_derived_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "derived_author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_derived_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_derived_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "derived_created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_deleted_by_agent_id_agents_id_fk": { + "name": "issue_comments_deleted_by_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "deleted_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_comments_deleted_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_deleted_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "deleted_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_create_idempotency_keys": { + "name": "issue_create_idempotency_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_create_idempotency_keys_company_key_uq": { + "name": "issue_create_idempotency_keys_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_create_idempotency_keys_issue_idx": { + "name": "issue_create_idempotency_keys_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_create_idempotency_keys_company_created_at_idx": { + "name": "issue_create_idempotency_keys_company_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_create_idempotency_keys_company_id_companies_id_fk": { + "name": "issue_create_idempotency_keys_company_id_companies_id_fk", + "tableFrom": "issue_create_idempotency_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_create_idempotency_keys_issue_id_issues_id_fk": { + "name": "issue_create_idempotency_keys_issue_id_issues_id_fk", + "tableFrom": "issue_create_idempotency_keys", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_execution_decisions": { + "name": "issue_execution_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_type": { + "name": "stage_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_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": { + "issue_execution_decisions_company_issue_idx": { + "name": "issue_execution_decisions_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_execution_decisions_stage_idx": { + "name": "issue_execution_decisions_stage_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_execution_decisions_company_id_companies_id_fk": { + "name": "issue_execution_decisions_company_id_companies_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_issue_id_issues_id_fk": { + "name": "issue_execution_decisions_issue_id_issues_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_execution_decisions_actor_agent_id_agents_id_fk": { + "name": "issue_execution_decisions_actor_agent_id_agents_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_inbox_archives": { + "name": "issue_inbox_archives", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_by_actor_type": { + "name": "archived_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "archived_by_agent_id": { + "name": "archived_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_by_run_id": { + "name": "archived_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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": { + "issue_inbox_archives_company_issue_idx": { + "name": "issue_inbox_archives_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_user_idx": { + "name": "issue_inbox_archives_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_issue_user_idx": { + "name": "issue_inbox_archives_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_inbox_archives_company_id_companies_id_fk": { + "name": "issue_inbox_archives_company_id_companies_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_inbox_archives_issue_id_issues_id_fk": { + "name": "issue_inbox_archives_issue_id_issues_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_inbox_archives_archived_by_agent_id_agents_id_fk": { + "name": "issue_inbox_archives_archived_by_agent_id_agents_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "agents", + "columnsFrom": [ + "archived_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_inbox_archives_archived_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_inbox_archives_archived_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "archived_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "issue_inbox_archives_archived_by_actor_type_check": { + "name": "issue_inbox_archives_archived_by_actor_type_check", + "value": "\"issue_inbox_archives\".\"archived_by_actor_type\" in ('user', 'agent')" + } + }, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_plan_decompositions": { + "name": "issue_plan_decompositions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accepted_plan_revision_id": { + "name": "accepted_plan_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accepted_interaction_id": { + "name": "accepted_interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'in_flight'" + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_child_count": { + "name": "requested_child_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_children": { + "name": "requested_children", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "child_issue_ids": { + "name": "child_issue_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_run_id": { + "name": "owner_run_id", + "type": "uuid", + "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": { + "issue_plan_decompositions_company_source_status_idx": { + "name": "issue_plan_decompositions_company_source_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_plan_decompositions_active_owner_idx": { + "name": "issue_plan_decompositions_active_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"issue_plan_decompositions\".\"status\" = 'in_flight'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_plan_decompositions_source_revision_uq": { + "name": "issue_plan_decompositions_source_revision_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted_plan_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_plan_decompositions_company_id_companies_id_fk": { + "name": "issue_plan_decompositions_company_id_companies_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_plan_decompositions_source_issue_id_issues_id_fk": { + "name": "issue_plan_decompositions_source_issue_id_issues_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_plan_decompositions_accepted_plan_revision_id_document_revisions_id_fk": { + "name": "issue_plan_decompositions_accepted_plan_revision_id_document_revisions_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "document_revisions", + "columnsFrom": [ + "accepted_plan_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_plan_decompositions_accepted_interaction_id_issue_thread_interactions_id_fk": { + "name": "issue_plan_decompositions_accepted_interaction_id_issue_thread_interactions_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "issue_thread_interactions", + "columnsFrom": [ + "accepted_interaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_plan_decompositions_owner_agent_id_agents_id_fk": { + "name": "issue_plan_decompositions_owner_agent_id_agents_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_plan_decompositions_owner_run_id_heartbeat_runs_id_fk": { + "name": "issue_plan_decompositions_owner_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_plan_decompositions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "owner_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_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": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_recovery_actions": { + "name": "issue_recovery_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recovery_issue_id": { + "name": "recovery_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_owner_agent_id": { + "name": "previous_owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "return_owner_agent_id": { + "name": "return_owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cause": { + "name": "cause", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wake_policy": { + "name": "wake_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "monitor_policy": { + "name": "monitor_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timeout_at": { + "name": "timeout_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 + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "issue_recovery_actions_company_source_status_idx": { + "name": "issue_recovery_actions_company_source_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_company_owner_status_idx": { + "name": "issue_recovery_actions_company_owner_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_company_recovery_issue_idx": { + "name": "issue_recovery_actions_company_recovery_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recovery_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_active_source_uq": { + "name": "issue_recovery_actions_active_source_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_recovery_actions\".\"status\" in ('active', 'escalated')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_recovery_actions_active_fingerprint_uq": { + "name": "issue_recovery_actions_active_fingerprint_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cause", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_recovery_actions\".\"status\" in ('active', 'escalated')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_recovery_actions_company_id_companies_id_fk": { + "name": "issue_recovery_actions_company_id_companies_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_recovery_actions_source_issue_id_issues_id_fk": { + "name": "issue_recovery_actions_source_issue_id_issues_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_recovery_actions_recovery_issue_id_issues_id_fk": { + "name": "issue_recovery_actions_recovery_issue_id_issues_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "issues", + "columnsFrom": [ + "recovery_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_recovery_actions_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_recovery_actions_previous_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_previous_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "agents", + "columnsFrom": [ + "previous_owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_recovery_actions_return_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_return_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "tableTo": "agents", + "columnsFrom": [ + "return_owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_reference_mentions": { + "name": "issue_reference_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_issue_id": { + "name": "target_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_record_id": { + "name": "source_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_text": { + "name": "matched_text", + "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": { + "issue_reference_mentions_company_source_issue_idx": { + "name": "issue_reference_mentions_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_target_issue_idx": { + "name": "issue_reference_mentions_company_target_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_issue_pair_idx": { + "name": "issue_reference_mentions_company_issue_pair_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_record_uq": { + "name": "issue_reference_mentions_company_source_mention_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_null_record_uq": { + "name": "issue_reference_mentions_company_source_mention_null_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_reference_mentions_company_id_companies_id_fk": { + "name": "issue_reference_mentions_company_id_companies_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_reference_mentions_source_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_source_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_reference_mentions_target_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_target_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "target_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_relations": { + "name": "issue_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "related_issue_id": { + "name": "related_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_relations_company_issue_idx": { + "name": "issue_relations_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_related_issue_idx": { + "name": "issue_relations_company_related_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_type_idx": { + "name": "issue_relations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_edge_uq": { + "name": "issue_relations_company_edge_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_relations_company_id_companies_id_fk": { + "name": "issue_relations_company_id_companies_id_fk", + "tableFrom": "issue_relations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_relations_issue_id_issues_id_fk": { + "name": "issue_relations_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_related_issue_id_issues_id_fk": { + "name": "issue_relations_related_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "related_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_created_by_agent_id_agents_id_fk": { + "name": "issue_relations_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_relations", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_thread_interactions": { + "name": "issue_thread_interactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "continuation_policy": { + "name": "continuation_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'wake_assignee'" + }, + "requested_resolver_policy": { + "name": "requested_resolver_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'anyone'" + }, + "effective_resolver_policy": { + "name": "effective_resolver_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'anyone'" + }, + "resolver_policy_provenance": { + "name": "resolver_policy_provenance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherited'" + }, + "effective_resolver_policy_source": { + "name": "effective_resolver_policy_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_comment_id": { + "name": "source_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "addressee_agent_id": { + "name": "addressee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_run_id": { + "name": "resolved_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "issue_thread_interactions_issue_idx": { + "name": "issue_thread_interactions_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_created_at_idx": { + "name": "issue_thread_interactions_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_status_idx": { + "name": "issue_thread_interactions_company_issue_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_idempotency_uq": { + "name": "issue_thread_interactions_company_issue_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_thread_interactions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_source_comment_idx": { + "name": "issue_thread_interactions_source_comment_idx", + "columns": [ + { + "expression": "source_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_addressee_agent_idx": { + "name": "issue_thread_interactions_addressee_agent_idx", + "columns": [ + { + "expression": "addressee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_thread_interactions_company_id_companies_id_fk": { + "name": "issue_thread_interactions_company_id_companies_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_issue_id_issues_id_fk": { + "name": "issue_thread_interactions_issue_id_issues_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_comment_id_issue_comments_id_fk": { + "name": "issue_thread_interactions_source_comment_id_issue_comments_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issue_comments", + "columnsFrom": [ + "source_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk": { + "name": "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_created_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_addressee_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_addressee_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "addressee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_resolved_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_resolved_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_resolved_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_thread_interactions_resolved_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "resolved_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_hold_members": { + "name": "issue_tree_hold_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "issue_identifier": { + "name": "issue_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_status": { + "name": "issue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_run_id": { + "name": "active_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_run_status": { + "name": "active_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skipped": { + "name": "skipped", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_hold_members_hold_issue_uq": { + "name": "issue_tree_hold_members_hold_issue_uq", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_company_issue_idx": { + "name": "issue_tree_hold_members_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_hold_depth_idx": { + "name": "issue_tree_hold_members_hold_depth_idx", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "depth", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_hold_members_company_id_companies_id_fk": { + "name": "issue_tree_hold_members_company_id_companies_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk": { + "name": "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issue_tree_holds", + "columnsFrom": [ + "hold_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_parent_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_parent_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_assignee_agent_id_agents_id_fk": { + "name": "issue_tree_hold_members_assignee_agent_id_agents_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "active_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_holds": { + "name": "issue_tree_holds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_issue_id": { + "name": "root_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_policy": { + "name": "release_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_by_actor_type": { + "name": "released_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_agent_id": { + "name": "released_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_by_user_id": { + "name": "released_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_run_id": { + "name": "released_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_metadata": { + "name": "release_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_holds_company_root_status_idx": { + "name": "issue_tree_holds_company_root_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_holds_company_status_mode_idx": { + "name": "issue_tree_holds_company_status_mode_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_holds_company_id_companies_id_fk": { + "name": "issue_tree_holds_company_id_companies_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_holds_root_issue_id_issues_id_fk": { + "name": "issue_tree_holds_root_issue_id_issues_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "issues", + "columnsFrom": [ + "root_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_released_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "released_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "released_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_watchdogs": { + "name": "issue_watchdogs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watchdog_agent_id": { + "name": "watchdog_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "watchdog_issue_id": { + "name": "watchdog_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_observed_fingerprint": { + "name": "last_observed_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_reviewed_fingerprint": { + "name": "last_reviewed_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_observed_stop_snapshot": { + "name": "last_observed_stop_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_reviewed_stop_snapshot": { + "name": "last_reviewed_stop_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_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 + }, + "trigger_count": { + "name": "trigger_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_run_id": { + "name": "updated_by_run_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": { + "issue_watchdogs_company_issue_uq": { + "name": "issue_watchdogs_company_issue_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_watchdogs_company_status_idx": { + "name": "issue_watchdogs_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_watchdogs_company_agent_idx": { + "name": "issue_watchdogs_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "watchdog_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_watchdogs_company_watchdog_issue_uq": { + "name": "issue_watchdogs_company_watchdog_issue_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "watchdog_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_watchdogs\".\"watchdog_issue_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_watchdogs_company_id_companies_id_fk": { + "name": "issue_watchdogs_company_id_companies_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_watchdogs_issue_id_issues_id_fk": { + "name": "issue_watchdogs_issue_id_issues_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_watchdogs_watchdog_agent_id_agents_id_fk": { + "name": "issue_watchdogs_watchdog_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "agents", + "columnsFrom": [ + "watchdog_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_watchdogs_watchdog_issue_id_issues_id_fk": { + "name": "issue_watchdogs_watchdog_issue_id_issues_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "issues", + "columnsFrom": [ + "watchdog_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_created_by_agent_id_agents_id_fk": { + "name": "issue_watchdogs_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_watchdogs_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_updated_by_agent_id_agents_id_fk": { + "name": "issue_watchdogs_updated_by_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_watchdogs_updated_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_watchdogs_updated_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_watchdogs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "updated_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_work_products": { + "name": "issue_work_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_service_id": { + "name": "runtime_service_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_state": { + "name": "review_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_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": { + "issue_work_products_company_issue_type_idx": { + "name": "issue_work_products_company_issue_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_execution_workspace_type_idx": { + "name": "issue_work_products_company_execution_workspace_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_provider_external_id_idx": { + "name": "issue_work_products_company_provider_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_updated_idx": { + "name": "issue_work_products_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_work_products_company_id_companies_id_fk": { + "name": "issue_work_products_company_id_companies_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_work_products_project_id_projects_id_fk": { + "name": "issue_work_products_project_id_projects_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_issue_id_issues_id_fk": { + "name": "issue_work_products_issue_id_issues_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_work_products_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issue_work_products_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk": { + "name": "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "workspace_runtime_services", + "columnsFrom": [ + "runtime_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_work_products_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "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": "'backlog'" + }, + "work_mode": { + "name": "work_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "harness_kind": { + "name": "harness_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "review_policy": { + "name": "review_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_fingerprint": { + "name": "origin_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_policy": { + "name": "execution_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_state": { + "name": "execution_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "monitor_next_check_at": { + "name": "monitor_next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_wake_requested_at": { + "name": "monitor_wake_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_last_triggered_at": { + "name": "monitor_last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_attempt_count": { + "name": "monitor_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "monitor_notes": { + "name": "monitor_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monitor_scheduled_by": { + "name": "monitor_scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_preference": { + "name": "execution_workspace_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_trust": { + "name": "source_trust", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "unblock_descriptor": { + "name": "unblock_descriptor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "blocked_transition_at": { + "name": "blocked_transition_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_owner_notified_at": { + "name": "blocked_owner_notified_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 + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_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": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_harness_kind_idx": { + "name": "issues_company_harness_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "harness_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_responsible_user_idx": { + "name": "issues_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_origin_idx": { + "name": "issues_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_workspace_idx": { + "name": "issues_company_project_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_execution_workspace_idx": { + "name": "issues_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_monitor_due_idx": { + "name": "issues_company_monitor_due_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "monitor_next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_updated_idx": { + "name": "issues_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_created_idx": { + "name": "issues_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_open_normalized_title_created_idx": { + "name": "issues_open_normalized_title_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(regexp_replace(btrim(\"title\"), '\\s+', ' ', 'g'))", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"issues\".\"hidden_at\" is null and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_priority_idx": { + "name": "issues_company_priority_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_title_search_idx": { + "name": "issues_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_identifier_search_idx": { + "name": "issues_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_description_search_idx": { + "name": "issues_description_search_idx", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_open_routine_execution_uq": { + "name": "issues_open_routine_execution_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'routine_execution'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"execution_run_id\" is not null\n and \"issues\".\"status\" in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_incident_uq": { + "name": "issues_active_liveness_recovery_incident_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_leaf_uq": { + "name": "issues_active_liveness_recovery_leaf_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_fingerprint\" <> 'default'\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stale_run_evaluation_uq": { + "name": "issues_active_stale_run_evaluation_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stale_active_run_evaluation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_task_watchdog_uq": { + "name": "issues_active_task_watchdog_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'task_watchdog'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_productivity_review_uq": { + "name": "issues_active_productivity_review_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'issue_productivity_review'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stranded_issue_recovery_uq": { + "name": "issues_active_stranded_issue_recovery_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stranded_issue_recovery'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_onboarding_first_task_uq": { + "name": "issues_onboarding_first_task_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'onboarding_first_task'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_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": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_user_uq": { + "name": "join_requests_pending_human_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requesting_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"requesting_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_email_uq": { + "name": "join_requests_pending_human_email_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"request_email_snapshot\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"request_email_snapshot\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "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": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_automation_executions": { + "name": "pipeline_automation_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggering_event_id": { + "name": "triggering_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_issue_id": { + "name": "execution_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retry_of_execution_id": { + "name": "retry_of_execution_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_automation_executions_idempotency_uq": { + "name": "pipeline_automation_executions_idempotency_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "triggering_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_company_case_idx": { + "name": "pipeline_automation_executions_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_routine_idx": { + "name": "pipeline_automation_executions_routine_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_execution_issue_idx": { + "name": "pipeline_automation_executions_execution_issue_idx", + "columns": [ + { + "expression": "execution_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_automation_executions_retry_of_execution_idx": { + "name": "pipeline_automation_executions_retry_of_execution_idx", + "columns": [ + { + "expression": "retry_of_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_automation_executions_company_id_companies_id_fk": { + "name": "pipeline_automation_executions_company_id_companies_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_automation_executions_case_id_pipeline_cases_id_fk": { + "name": "pipeline_automation_executions_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_automation_executions_routine_id_routines_id_fk": { + "name": "pipeline_automation_executions_routine_id_routines_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_automation_executions_execution_issue_id_issues_id_fk": { + "name": "pipeline_automation_executions_execution_issue_id_issues_id_fk", + "tableFrom": "pipeline_automation_executions", + "tableTo": "issues", + "columnsFrom": [ + "execution_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_automation_executions_status_check": { + "name": "pipeline_automation_executions_status_check", + "value": "\"pipeline_automation_executions\".\"status\" in ('succeeded', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_case_blockers": { + "name": "pipeline_case_blockers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blocked_by_case_id": { + "name": "blocked_by_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_case_blockers_case_blocked_by_uq": { + "name": "pipeline_case_blockers_case_blocked_by_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blocked_by_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_blockers_blocked_by_idx": { + "name": "pipeline_case_blockers_blocked_by_idx", + "columns": [ + { + "expression": "blocked_by_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_blockers_company_case_idx": { + "name": "pipeline_case_blockers_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_blockers_company_id_companies_id_fk": { + "name": "pipeline_case_blockers_company_id_companies_id_fk", + "tableFrom": "pipeline_case_blockers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_blockers_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_blockers_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_blockers", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_blockers_blocked_by_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_blockers_blocked_by_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_blockers", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "blocked_by_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_case_blockers_no_self_block_check": { + "name": "pipeline_case_blockers_no_self_block_check", + "value": "\"pipeline_case_blockers\".\"case_id\" <> \"pipeline_case_blockers\".\"blocked_by_case_id\"" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_case_documents": { + "name": "pipeline_case_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "pipeline_case_documents_company_case_key_uq": { + "name": "pipeline_case_documents_company_case_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_documents_document_uq": { + "name": "pipeline_case_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_documents_company_case_updated_idx": { + "name": "pipeline_case_documents_company_case_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_documents_company_id_companies_id_fk": { + "name": "pipeline_case_documents_company_id_companies_id_fk", + "tableFrom": "pipeline_case_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_documents_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_documents_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_documents", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_documents_document_id_documents_id_fk": { + "name": "pipeline_case_documents_document_id_documents_id_fk", + "tableFrom": "pipeline_case_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_case_events": { + "name": "pipeline_case_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "from_stage_id": { + "name": "from_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_stage_id": { + "name": "to_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipeline_case_events_case_created_idx": { + "name": "pipeline_case_events_case_created_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_events_company_case_idx": { + "name": "pipeline_case_events_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_events_company_id_companies_id_fk": { + "name": "pipeline_case_events_company_id_companies_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_events_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_events_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_events_actor_agent_id_agents_id_fk": { + "name": "pipeline_case_events_actor_agent_id_agents_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_case_events_from_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_case_events_from_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "from_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_case_events_to_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_case_events_to_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_case_events", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "to_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_case_events_type_check": { + "name": "pipeline_case_events_type_check", + "value": "\"pipeline_case_events\".\"type\" in (\n 'ingested',\n 'updated',\n 'claimed',\n 'lease_released',\n 'lease_expired',\n 'transitioned',\n 'transition_forced',\n 'transition_suggested',\n 'suggestion_resolved',\n 'review_decided',\n 'conversation_opened',\n 'issue_linked',\n 'issue_unlinked',\n 'automation_executed',\n 'automation_failed',\n 'automation_retry_requested',\n 'automation_effects_retired',\n 'automation_retry_dispatched',\n 'blockers_set',\n 'blockers_resolved',\n 'children_terminal',\n 'upstream_drift',\n 'drift_acknowledged'\n )" + }, + "pipeline_case_events_actor_type_check": { + "name": "pipeline_case_events_actor_type_check", + "value": "\"pipeline_case_events\".\"actor_type\" in ('user', 'agent', 'system')" + }, + "pipeline_case_events_agent_run_check": { + "name": "pipeline_case_events_agent_run_check", + "value": "\"pipeline_case_events\".\"actor_type\" <> 'agent' or \"pipeline_case_events\".\"run_id\" is not null" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_case_issue_links": { + "name": "pipeline_case_issue_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "automation_attempt_id": { + "name": "automation_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_by_attempt_id": { + "name": "retired_by_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retired_reason": { + "name": "retired_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": { + "pipeline_case_issue_links_case_issue_uq": { + "name": "pipeline_case_issue_links_case_issue_uq", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_issue_links_issue_idx": { + "name": "pipeline_case_issue_links_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_issue_links_company_case_idx": { + "name": "pipeline_case_issue_links_company_case_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_case_issue_links_automation_attempt_idx": { + "name": "pipeline_case_issue_links_automation_attempt_idx", + "columns": [ + { + "expression": "automation_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_case_issue_links_company_id_companies_id_fk": { + "name": "pipeline_case_issue_links_company_id_companies_id_fk", + "tableFrom": "pipeline_case_issue_links", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_issue_links_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_issue_links_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_issue_links", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_case_issue_links_issue_id_issues_id_fk": { + "name": "pipeline_case_issue_links_issue_id_issues_id_fk", + "tableFrom": "pipeline_case_issue_links", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_case_issue_links_role_check": { + "name": "pipeline_case_issue_links_role_check", + "value": "\"pipeline_case_issue_links\".\"role\" in ('origin', 'conversation', 'work', 'automation')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_cases": { + "name": "pipeline_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "case_key": { + "name": "case_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "workspace_ref": { + "name": "workspace_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parent_case_id": { + "name": "parent_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_case_version": { + "name": "parent_case_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_key": { + "name": "request_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_attempt_id": { + "name": "automation_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "pending_suggestion": { + "name": "pending_suggestion", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lease_owner_type": { + "name": "lease_owner_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_agent_id": { + "name": "lease_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_user_id": { + "name": "lease_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "terminal_kind": { + "name": "terminal_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_by_attempt_id": { + "name": "retired_by_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retired_reason": { + "name": "retired_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hidden_from_board_at": { + "name": "hidden_from_board_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "child_count": { + "name": "child_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "terminal_child_count": { + "name": "terminal_child_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_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": { + "pipeline_cases_pipeline_case_key_uq": { + "name": "pipeline_cases_pipeline_case_key_uq", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "case_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_parent_request_key_uq": { + "name": "pipeline_cases_parent_request_key_uq", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"pipeline_cases\".\"request_key\" is not null and \"pipeline_cases\".\"retired_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_company_idx": { + "name": "pipeline_cases_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_pipeline_stage_idx": { + "name": "pipeline_cases_pipeline_stage_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_parent_idx": { + "name": "pipeline_cases_parent_idx", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_automation_attempt_idx": { + "name": "pipeline_cases_automation_attempt_idx", + "columns": [ + { + "expression": "automation_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_retired_idx": { + "name": "pipeline_cases_retired_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retired_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_cases_lease_expires_idx": { + "name": "pipeline_cases_lease_expires_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pipeline_cases\".\"lease_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_cases_company_id_companies_id_fk": { + "name": "pipeline_cases_company_id_companies_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_cases_pipeline_id_pipelines_id_fk": { + "name": "pipeline_cases_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_cases_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_cases_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pipeline_cases_parent_case_id_pipeline_cases_id_fk": { + "name": "pipeline_cases_parent_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "pipeline_cases", + "columnsFrom": [ + "parent_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_cases_lease_agent_id_agents_id_fk": { + "name": "pipeline_cases_lease_agent_id_agents_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "agents", + "columnsFrom": [ + "lease_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipeline_cases_created_by_agent_id_agents_id_fk": { + "name": "pipeline_cases_created_by_agent_id_agents_id_fk", + "tableFrom": "pipeline_cases", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_cases_terminal_kind_check": { + "name": "pipeline_cases_terminal_kind_check", + "value": "\"pipeline_cases\".\"terminal_kind\" is null or \"pipeline_cases\".\"terminal_kind\" in ('done', 'cancelled')" + }, + "pipeline_cases_lease_owner_type_check": { + "name": "pipeline_cases_lease_owner_type_check", + "value": "\"pipeline_cases\".\"lease_owner_type\" is null or \"pipeline_cases\".\"lease_owner_type\" in ('user', 'agent')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_documents": { + "name": "pipeline_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "pipeline_documents_company_pipeline_key_uq": { + "name": "pipeline_documents_company_pipeline_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_documents_document_uq": { + "name": "pipeline_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_documents_company_pipeline_updated_idx": { + "name": "pipeline_documents_company_pipeline_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_documents_company_id_companies_id_fk": { + "name": "pipeline_documents_company_id_companies_id_fk", + "tableFrom": "pipeline_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_documents_pipeline_id_pipelines_id_fk": { + "name": "pipeline_documents_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_documents", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_documents_document_id_documents_id_fk": { + "name": "pipeline_documents_document_id_documents_id_fk", + "tableFrom": "pipeline_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipeline_stages": { + "name": "pipeline_stages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "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": { + "pipeline_stages_pipeline_key_uq": { + "name": "pipeline_stages_pipeline_key_uq", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_stages_pipeline_position_idx": { + "name": "pipeline_stages_pipeline_position_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_stages_pipeline_id_pipelines_id_fk": { + "name": "pipeline_stages_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_stages", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pipeline_stages_kind_check": { + "name": "pipeline_stages_kind_check", + "value": "\"pipeline_stages\".\"kind\" in ('working', 'review', 'done', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.pipeline_transitions": { + "name": "pipeline_transitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "pipeline_id": { + "name": "pipeline_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "from_stage_id": { + "name": "from_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "to_stage_id": { + "name": "to_stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "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": { + "pipeline_transitions_pipeline_edge_uq": { + "name": "pipeline_transitions_pipeline_edge_uq", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_transitions_pipeline_from_idx": { + "name": "pipeline_transitions_pipeline_from_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "from_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipeline_transitions_pipeline_to_idx": { + "name": "pipeline_transitions_pipeline_to_idx", + "columns": [ + { + "expression": "pipeline_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to_stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipeline_transitions_pipeline_id_pipelines_id_fk": { + "name": "pipeline_transitions_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_transitions", + "tableTo": "pipelines", + "columnsFrom": [ + "pipeline_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_transitions_from_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_transitions_from_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_transitions", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "from_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipeline_transitions_to_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_transitions_to_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_transitions", + "tableTo": "pipeline_stages", + "columnsFrom": [ + "to_stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pipelines": { + "name": "pipelines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enforce_transitions": { + "name": "enforce_transitions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pipelines_company_key_uq": { + "name": "pipelines_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipelines_company_idx": { + "name": "pipelines_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pipelines_company_project_idx": { + "name": "pipelines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pipelines_company_id_companies_id_fk": { + "name": "pipelines_company_id_companies_id_fk", + "tableFrom": "pipelines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pipelines_project_id_projects_id_fk": { + "name": "pipelines_project_id_projects_id_fk", + "tableFrom": "pipelines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "pipelines_created_by_agent_id_agents_id_fk": { + "name": "pipelines_created_by_agent_id_agents_id_fk", + "tableFrom": "pipelines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_company_idx": { + "name": "plugin_config_plugin_company_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_config_company_id_companies_id_fk": { + "name": "plugin_config_company_id_companies_id_fk", + "tableFrom": "plugin_config", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_database_namespaces": { + "name": "plugin_database_namespaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_mode": { + "name": "namespace_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'schema'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_database_namespaces_plugin_idx": { + "name": "plugin_database_namespaces_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_namespace_idx": { + "name": "plugin_database_namespaces_namespace_idx", + "columns": [ + { + "expression": "namespace_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_status_idx": { + "name": "plugin_database_namespaces_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_database_namespaces_plugin_id_plugins_id_fk": { + "name": "plugin_database_namespaces_plugin_id_plugins_id_fk", + "tableFrom": "plugin_database_namespaces", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "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": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_company_idx": { + "name": "plugin_entities_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_entities_company_id_companies_id_fk": { + "name": "plugin_entities_company_id_companies_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "nullsNotDistinct": true, + "columns": [ + "company_id", + "plugin_id", + "entity_type", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_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": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_company_idx": { + "name": "plugin_job_runs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_company_id_companies_id_fk": { + "name": "plugin_job_runs_company_id_companies_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_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": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_company_idx": { + "name": "plugin_logs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_logs_company_id_companies_id_fk": { + "name": "plugin_logs_company_id_companies_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_managed_resources": { + "name": "plugin_managed_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_kind": { + "name": "resource_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "defaults_json": { + "name": "defaults_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": { + "plugin_managed_resources_company_idx": { + "name": "plugin_managed_resources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_plugin_idx": { + "name": "plugin_managed_resources_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_resource_idx": { + "name": "plugin_managed_resources_resource_idx", + "columns": [ + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_company_plugin_resource_uq": { + "name": "plugin_managed_resources_company_plugin_resource_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_managed_resources_company_id_companies_id_fk": { + "name": "plugin_managed_resources_company_id_companies_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_managed_resources_plugin_id_plugins_id_fk": { + "name": "plugin_managed_resources_plugin_id_plugins_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_key": { + "name": "migration_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "plugin_migrations_plugin_key_idx": { + "name": "plugin_migrations_plugin_key_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_plugin_idx": { + "name": "plugin_migrations_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_status_idx": { + "name": "plugin_migrations_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_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": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_company_idx": { + "name": "plugin_webhook_deliveries_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_webhook_deliveries_company_id_companies_id_fk": { + "name": "plugin_webhook_deliveries_company_id_companies_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_memberships": { + "name": "project_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'joined'" + }, + "starred_at": { + "name": "starred_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": { + "project_memberships_company_user_idx": { + "name": "project_memberships_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_memberships_company_user_starred_idx": { + "name": "project_memberships_company_user_starred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_memberships_project_idx": { + "name": "project_memberships_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_memberships_company_user_project_uq": { + "name": "project_memberships_company_user_project_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_memberships_company_id_companies_id_fk": { + "name": "project_memberships_company_id_companies_id_fk", + "tableFrom": "project_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_memberships_project_id_projects_id_fk": { + "name": "project_memberships_project_id_projects_id_fk", + "tableFrom": "project_memberships", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ref": { + "name": "default_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "setup_command": { + "name": "setup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_command": { + "name": "cleanup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_provider": { + "name": "remote_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_workspace_ref": { + "name": "remote_workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_workspace_key": { + "name": "shared_workspace_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "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": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_source_type_idx": { + "name": "project_workspaces_project_source_type_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_company_shared_key_idx": { + "name": "project_workspaces_company_shared_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_workspace_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_remote_ref_idx": { + "name": "project_workspaces_project_remote_ref_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_workspace_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_documents": { + "name": "routine_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "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": { + "routine_documents_company_routine_key_uq": { + "name": "routine_documents_company_routine_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_documents_document_uq": { + "name": "routine_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_documents_company_routine_updated_idx": { + "name": "routine_documents_company_routine_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_documents_company_id_companies_id_fk": { + "name": "routine_documents_company_id_companies_id_fk", + "tableFrom": "routine_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routine_documents_routine_id_routines_id_fk": { + "name": "routine_documents_routine_id_routines_id_fk", + "tableFrom": "routine_documents", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_documents_document_id_documents_id_fk": { + "name": "routine_documents_document_id_documents_id_fk", + "tableFrom": "routine_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_revisions": { + "name": "routine_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "restored_from_revision_id": { + "name": "restored_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_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": { + "routine_revisions_routine_revision_uq": { + "name": "routine_revisions_routine_revision_uq", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_revisions_company_routine_created_idx": { + "name": "routine_revisions_company_routine_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_revisions_company_responsible_user_idx": { + "name": "routine_revisions_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_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": { + "routine_revisions_company_id_companies_id_fk": { + "name": "routine_revisions_company_id_companies_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_revisions_routine_id_routines_id_fk": { + "name": "routine_revisions_routine_id_routines_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_revisions_restored_from_revision_id_routine_revisions_id_fk": { + "name": "routine_revisions_restored_from_revision_id_routine_revisions_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "routine_revisions", + "columnsFrom": [ + "restored_from_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_revisions_created_by_agent_id_agents_id_fk": { + "name": "routine_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "routine_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "routine_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "routine_revision_id": { + "name": "routine_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_payload": { + "name": "trigger_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dispatch_fingerprint": { + "name": "dispatch_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_issue_id": { + "name": "linked_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "coalesced_into_run_id": { + "name": "coalesced_into_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "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": { + "routine_runs_company_routine_idx": { + "name": "routine_runs_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_revision_idx": { + "name": "routine_runs_revision_idx", + "columns": [ + { + "expression": "routine_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_company_responsible_user_idx": { + "name": "routine_runs_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idx": { + "name": "routine_runs_trigger_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_dispatch_fingerprint_idx": { + "name": "routine_runs_dispatch_fingerprint_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatch_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_linked_issue_idx": { + "name": "routine_runs_linked_issue_idx", + "columns": [ + { + "expression": "linked_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idempotency_idx": { + "name": "routine_runs_trigger_idempotency_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_runs_company_id_companies_id_fk": { + "name": "routine_runs_company_id_companies_id_fk", + "tableFrom": "routine_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_trigger_id_routine_triggers_id_fk": { + "name": "routine_runs_trigger_id_routine_triggers_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_routine_revision_id_routine_revisions_id_fk": { + "name": "routine_runs_routine_revision_id_routine_revisions_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_revisions", + "columnsFrom": [ + "routine_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_linked_issue_id_issues_id_fk": { + "name": "routine_runs_linked_issue_id_issues_id_fk", + "tableFrom": "routine_runs", + "tableTo": "issues", + "columnsFrom": [ + "linked_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_triggers": { + "name": "routine_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "signing_mode": { + "name": "signing_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replay_window_sec": { + "name": "replay_window_sec", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_triggers_company_routine_idx": { + "name": "routine_triggers_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_company_kind_idx": { + "name": "routine_triggers_company_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_next_run_idx": { + "name": "routine_triggers_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_idx": { + "name": "routine_triggers_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_uq": { + "name": "routine_triggers_public_id_uq", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_triggers_company_id_companies_id_fk": { + "name": "routine_triggers_company_id_companies_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_routine_id_routines_id_fk": { + "name": "routine_triggers_routine_id_routines_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_secret_id_company_secrets_id_fk": { + "name": "routine_triggers_secret_id_company_secrets_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_created_by_agent_id_agents_id_fk": { + "name": "routine_triggers_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_updated_by_agent_id_agents_id_fk": { + "name": "routine_triggers_updated_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'coalesce_if_active'" + }, + "catch_up_policy": { + "name": "catch_up_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip_missed'" + }, + "activity_gate_policy": { + "name": "activity_gate_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'always'" + }, + "activity_gate_scope": { + "name": "activity_gate_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_enqueued_at": { + "name": "last_enqueued_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": { + "routines_company_status_idx": { + "name": "routines_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_assignee_idx": { + "name": "routines_company_assignee_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_project_idx": { + "name": "routines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_folder_idx": { + "name": "routines_company_folder_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_responsible_user_idx": { + "name": "routines_company_responsible_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "responsible_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_origin_idx": { + "name": "routines_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routines_company_id_companies_id_fk": { + "name": "routines_company_id_companies_id_fk", + "tableFrom": "routines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_project_id_projects_id_fk": { + "name": "routines_project_id_projects_id_fk", + "tableFrom": "routines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_folder_id_folders_id_fk": { + "name": "routines_folder_id_folders_id_fk", + "tableFrom": "routines", + "tableTo": "folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_goal_id_goals_id_fk": { + "name": "routines_goal_id_goals_id_fk", + "tableFrom": "routines", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_parent_issue_id_issues_id_fk": { + "name": "routines_parent_issue_id_issues_id_fk", + "tableFrom": "routines", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_assignee_agent_id_agents_id_fk": { + "name": "routines_assignee_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routines_created_by_agent_id_agents_id_fk": { + "name": "routines_created_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_updated_by_agent_id_agents_id_fk": { + "name": "routines_updated_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_access_events": { + "name": "secret_access_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_secret_definition_id": { + "name": "user_secret_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "responsible_user_id": { + "name": "responsible_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_owner_user_id": { + "name": "credential_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_subject_type": { + "name": "credential_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_subject_id": { + "name": "credential_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumer_type": { + "name": "consumer_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer_id": { + "name": "consumer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_access_events_company_created_idx": { + "name": "secret_access_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_secret_created_idx": { + "name": "secret_access_events_secret_created_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_user_definition_created_idx": { + "name": "secret_access_events_user_definition_created_idx", + "columns": [ + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_company_credential_owner_idx": { + "name": "secret_access_events_company_credential_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_consumer_idx": { + "name": "secret_access_events_consumer_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_access_events_run_idx": { + "name": "secret_access_events_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_access_events_company_id_companies_id_fk": { + "name": "secret_access_events_company_id_companies_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "secret_access_events_secret_id_company_secrets_id_fk": { + "name": "secret_access_events_secret_id_company_secrets_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk": { + "name": "secret_access_events_user_secret_definition_id_user_secret_definitions_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "user_secret_definitions", + "columnsFrom": [ + "user_secret_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "secret_access_events_issue_id_issues_id_fk": { + "name": "secret_access_events_issue_id_issues_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "secret_access_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "secret_access_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "secret_access_events_plugin_id_plugins_id_fk": { + "name": "secret_access_events_plugin_id_plugins_id_fk", + "tableFrom": "secret_access_events", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.smoke_run_steps": { + "name": "smoke_run_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scenario_step": { + "name": "scenario_step", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "screenshot_artifact_ref": { + "name": "screenshot_artifact_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_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": { + "smoke_run_steps_company_run_idx": { + "name": "smoke_run_steps_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "smoke_run_steps_company_path_idx": { + "name": "smoke_run_steps_company_path_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "smoke_run_steps_company_id_companies_id_fk": { + "name": "smoke_run_steps_company_id_companies_id_fk", + "tableFrom": "smoke_run_steps", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "smoke_run_steps_run_id_smoke_runs_id_fk": { + "name": "smoke_run_steps_run_id_smoke_runs_id_fk", + "tableFrom": "smoke_run_steps", + "tableTo": "smoke_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.smoke_runs": { + "name": "smoke_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "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": { + "smoke_runs_company_started_idx": { + "name": "smoke_runs_company_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "smoke_runs_company_status_idx": { + "name": "smoke_runs_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "smoke_runs_company_id_companies_id_fk": { + "name": "smoke_runs_company_id_companies_id_fk", + "tableFrom": "smoke_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_card_updates": { + "name": "status_card_updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "card_id": { + "name": "card_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_issue_id": { + "name": "generation_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "query_version": { + "name": "query_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "status_card_updates_card_started_idx": { + "name": "status_card_updates_card_started_idx", + "columns": [ + { + "expression": "card_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "status_card_updates_generation_issue_idx": { + "name": "status_card_updates_generation_issue_idx", + "columns": [ + { + "expression": "generation_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "status_card_updates_card_id_status_cards_id_fk": { + "name": "status_card_updates_card_id_status_cards_id_fk", + "tableFrom": "status_card_updates", + "tableTo": "status_cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_card_updates_generation_issue_id_issues_id_fk": { + "name": "status_card_updates_generation_issue_id_issues_id_fk", + "tableFrom": "status_card_updates", + "tableTo": "issues", + "columnsFrom": [ + "generation_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_card_updates_run_id_heartbeat_runs_id_fk": { + "name": "status_card_updates_run_id_heartbeat_runs_id_fk", + "tableFrom": "status_card_updates", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_cards": { + "name": "status_cards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title_pinned": { + "name": "title_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "interest_prompt": { + "name": "interest_prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queries": { + "name": "queries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "query_version": { + "name": "query_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "query_compiled_at": { + "name": "query_compiled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "query_compiled_by_agent_id": { + "name": "query_compiled_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "refresh_policy": { + "name": "refresh_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compiling'" + }, + "pending_change_count": { + "name": "pending_change_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pending_change_hash": { + "name": "pending_change_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_change_at": { + "name": "last_change_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "fingerprint_at": { + "name": "fingerprint_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "mentioned_issue_ids": { + "name": "mentioned_issue_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_update_run_kind": { + "name": "last_update_run_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_generated_at": { + "name": "last_generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "generating_issue_id": { + "name": "generating_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_eval_at": { + "name": "next_eval_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_by_user_id": { + "name": "archived_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_by_agent_id": { + "name": "archived_by_agent_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": { + "status_cards_company_archived_idx": { + "name": "status_cards_company_archived_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "status_cards_company_next_eval_idx": { + "name": "status_cards_company_next_eval_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_eval_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "status_cards_company_id_companies_id_fk": { + "name": "status_cards_company_id_companies_id_fk", + "tableFrom": "status_cards", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_cards_created_by_agent_id_agents_id_fk": { + "name": "status_cards_created_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_query_compiled_by_agent_id_agents_id_fk": { + "name": "status_cards_query_compiled_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "query_compiled_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_agent_id_agents_id_fk": { + "name": "status_cards_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_document_id_documents_id_fk": { + "name": "status_cards_document_id_documents_id_fk", + "tableFrom": "status_cards", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_generating_issue_id_issues_id_fk": { + "name": "status_cards_generating_issue_id_issues_id_fk", + "tableFrom": "status_cards", + "tableTo": "issues", + "columnsFrom": [ + "generating_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "status_cards_archived_by_agent_id_agents_id_fk": { + "name": "status_cards_archived_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "tableTo": "agents", + "columnsFrom": [ + "archived_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summary_slots": { + "name": "summary_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slot_key": { + "name": "slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "generating_issue_id": { + "name": "generating_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_generated_at": { + "name": "last_generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_generated_by_agent_id": { + "name": "last_generated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "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": { + "summary_slots_document_uq": { + "name": "summary_slots_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_slots_company_scope_idx": { + "name": "summary_slots_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_slots_company_generating_issue_idx": { + "name": "summary_slots_company_generating_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generating_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "summary_slots_company_updated_idx": { + "name": "summary_slots_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "summary_slots_company_id_companies_id_fk": { + "name": "summary_slots_company_id_companies_id_fk", + "tableFrom": "summary_slots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "summary_slots_document_id_documents_id_fk": { + "name": "summary_slots_document_id_documents_id_fk", + "tableFrom": "summary_slots", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "summary_slots_generating_issue_id_issues_id_fk": { + "name": "summary_slots_generating_issue_id_issues_id_fk", + "tableFrom": "summary_slots", + "tableTo": "issues", + "columnsFrom": [ + "generating_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "summary_slots_last_generated_by_agent_id_agents_id_fk": { + "name": "summary_slots_last_generated_by_agent_id_agents_id_fk", + "tableFrom": "summary_slots", + "tableTo": "agents", + "columnsFrom": [ + "last_generated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "summary_slots_company_scope_slot_uq": { + "name": "summary_slots_company_scope_slot_uq", + "nullsNotDistinct": true, + "columns": [ + "company_id", + "scope_kind", + "scope_id", + "slot_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_access_audit_events": { + "name": "tool_access_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "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": { + "tool_access_audit_company_created_idx": { + "name": "tool_access_audit_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_access_audit_connection_idx": { + "name": "tool_access_audit_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_access_audit_gateway_idx": { + "name": "tool_access_audit_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_access_audit_events_company_id_companies_id_fk": { + "name": "tool_access_audit_events_company_id_companies_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_access_audit_events_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_access_audit_events_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_access_audit_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_access_audit_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_access_audit_events_connection_id_tool_connections_id_fk": { + "name": "tool_access_audit_events_connection_id_tool_connections_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_access_audit_events_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_access_audit_events_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_access_audit_events", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_action_requests": { + "name": "tool_action_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_id": { + "name": "invocation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interaction_id": { + "name": "interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "canonical_arguments_hash": { + "name": "canonical_arguments_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_arguments_summary": { + "name": "canonical_arguments_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "signed_arguments": { + "name": "signed_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_markdown": { + "name": "preview_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_agent_id": { + "name": "decided_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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": { + "tool_action_requests_company_status_idx": { + "name": "tool_action_requests_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_action_requests_invocation_idx": { + "name": "tool_action_requests_invocation_idx", + "columns": [ + { + "expression": "invocation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_action_requests_issue_idx": { + "name": "tool_action_requests_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_action_requests_company_id_companies_id_fk": { + "name": "tool_action_requests_company_id_companies_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_action_requests_invocation_id_tool_invocations_id_fk": { + "name": "tool_action_requests_invocation_id_tool_invocations_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "tool_invocations", + "columnsFrom": [ + "invocation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_action_requests_issue_id_issues_id_fk": { + "name": "tool_action_requests_issue_id_issues_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_interaction_id_issue_thread_interactions_id_fk": { + "name": "tool_action_requests_interaction_id_issue_thread_interactions_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "issue_thread_interactions", + "columnsFrom": [ + "interaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_approval_id_approvals_id_fk": { + "name": "tool_action_requests_approval_id_approvals_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_requested_by_agent_id_agents_id_fk": { + "name": "tool_action_requests_requested_by_agent_id_agents_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_resolved_by_agent_id_agents_id_fk": { + "name": "tool_action_requests_resolved_by_agent_id_agents_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_action_requests_decided_by_agent_id_agents_id_fk": { + "name": "tool_action_requests_decided_by_agent_id_agents_id_fk", + "tableFrom": "tool_action_requests", + "tableTo": "agents", + "columnsFrom": [ + "decided_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_applications": { + "name": "tool_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_key": { + "name": "application_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "tool_applications_company_idx": { + "name": "tool_applications_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_applications_company_status_idx": { + "name": "tool_applications_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_applications_company_name_uq": { + "name": "tool_applications_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_applications_company_key_uq": { + "name": "tool_applications_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "application_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_applications_company_id_companies_id_fk": { + "name": "tool_applications_company_id_companies_id_fk", + "tableFrom": "tool_applications", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_applications_plugin_id_plugins_id_fk": { + "name": "tool_applications_plugin_id_plugins_id_fk", + "tableFrom": "tool_applications", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_applications_owner_agent_id_agents_id_fk": { + "name": "tool_applications_owner_agent_id_agents_id_fk", + "tableFrom": "tool_applications", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_call_events": { + "name": "tool_call_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_type": { + "name": "client_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_id": { + "name": "client_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_session_id": { + "name": "mcp_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invocation_id": { + "name": "invocation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action_request_id": { + "name": "action_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_slot_id": { + "name": "runtime_slot_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_policy_ids": { + "name": "matched_policy_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy_explanation": { + "name": "policy_explanation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_scope_summary": { + "name": "credential_scope_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "header_policy_summary": { + "name": "header_policy_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "arguments_summary": { + "name": "arguments_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_summary": { + "name": "request_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_hash": { + "name": "result_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_size_bytes": { + "name": "result_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "redaction_plan": { + "name": "redaction_plan", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rate_limit_state": { + "name": "rate_limit_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "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()" + } + }, + "indexes": { + "tool_call_events_company_created_idx": { + "name": "tool_call_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_run_idx": { + "name": "tool_call_events_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_issue_idx": { + "name": "tool_call_events_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_invocation_idx": { + "name": "tool_call_events_invocation_idx", + "columns": [ + { + "expression": "invocation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_call_events_gateway_idx": { + "name": "tool_call_events_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_call_events_company_id_companies_id_fk": { + "name": "tool_call_events_company_id_companies_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_call_events_agent_id_agents_id_fk": { + "name": "tool_call_events_agent_id_agents_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_run_id_heartbeat_runs_id_fk": { + "name": "tool_call_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_issue_id_issues_id_fk": { + "name": "tool_call_events_issue_id_issues_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_call_events_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_call_events_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_application_id_tool_applications_id_fk": { + "name": "tool_call_events_application_id_tool_applications_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_connection_id_tool_connections_id_fk": { + "name": "tool_call_events_connection_id_tool_connections_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_call_events_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_invocation_id_tool_invocations_id_fk": { + "name": "tool_call_events_invocation_id_tool_invocations_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_invocations", + "columnsFrom": [ + "invocation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_action_request_id_tool_action_requests_id_fk": { + "name": "tool_call_events_action_request_id_tool_action_requests_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_action_requests", + "columnsFrom": [ + "action_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_call_events_runtime_slot_id_tool_runtime_slots_id_fk": { + "name": "tool_call_events_runtime_slot_id_tool_runtime_slots_id_fk", + "tableFrom": "tool_call_events", + "tableTo": "tool_runtime_slots", + "columnsFrom": [ + "runtime_slot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_catalog_entries": { + "name": "tool_catalog_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entry_kind": { + "name": "entry_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'tool'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "output_schema": { + "name": "output_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "annotations": { + "name": "annotations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read'" + }, + "is_read_only": { + "name": "is_read_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_write": { + "name": "is_write", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_destructive": { + "name": "is_destructive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version_hash": { + "name": "version_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_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()" + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_agent_id": { + "name": "reviewed_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_user_id": { + "name": "reviewed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quarantined_at": { + "name": "quarantined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "quarantine_reason": { + "name": "quarantine_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": { + "tool_catalog_entries_company_idx": { + "name": "tool_catalog_entries_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_application_idx": { + "name": "tool_catalog_entries_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_connection_idx": { + "name": "tool_catalog_entries_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_company_status_idx": { + "name": "tool_catalog_entries_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_catalog_entries_connection_name_uq": { + "name": "tool_catalog_entries_connection_name_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_catalog_entries_company_id_companies_id_fk": { + "name": "tool_catalog_entries_company_id_companies_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_catalog_entries_application_id_tool_applications_id_fk": { + "name": "tool_catalog_entries_application_id_tool_applications_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_catalog_entries_connection_id_tool_connections_id_fk": { + "name": "tool_catalog_entries_connection_id_tool_connections_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_catalog_entries_reviewed_by_agent_id_agents_id_fk": { + "name": "tool_catalog_entries_reviewed_by_agent_id_agents_id_fk", + "tableFrom": "tool_catalog_entries", + "tableTo": "agents", + "columnsFrom": [ + "reviewed_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_connection_installs": { + "name": "tool_connection_installs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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": { + "tool_connection_installs_company_target_idx": { + "name": "tool_connection_installs_company_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connection_installs_connection_idx": { + "name": "tool_connection_installs_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connection_installs_target_uq": { + "name": "tool_connection_installs_target_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_connection_installs_company_id_companies_id_fk": { + "name": "tool_connection_installs_company_id_companies_id_fk", + "tableFrom": "tool_connection_installs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_connection_installs_connection_id_tool_connections_id_fk": { + "name": "tool_connection_installs_connection_id_tool_connections_id_fk", + "tableFrom": "tool_connection_installs", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_connection_installs_created_by_agent_id_agents_id_fk": { + "name": "tool_connection_installs_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_connection_installs", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "tool_connection_installs_target_type_check": { + "name": "tool_connection_installs_target_type_check", + "value": "\"tool_connection_installs\".\"target_type\" in ('company', 'agent')" + } + }, + "isRLSEnabled": false + }, + "public.tool_connections": { + "name": "tool_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'managed'" + }, + "ownership": { + "name": "ownership", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'customer'" + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "transport_config": { + "name": "transport_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "credential_refs": { + "name": "credential_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "credential_secret_refs": { + "name": "credential_secret_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unchecked'" + }, + "health_message": { + "name": "health_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_checked_at": { + "name": "health_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_health_at": { + "name": "last_health_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_catalog_refresh_at": { + "name": "last_catalog_refresh_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_connections_company_idx": { + "name": "tool_connections_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connections_application_idx": { + "name": "tool_connections_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connections_company_enabled_idx": { + "name": "tool_connections_company_enabled_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_connections_company_uid_uq": { + "name": "tool_connections_company_uid_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_connections_company_id_companies_id_fk": { + "name": "tool_connections_company_id_companies_id_fk", + "tableFrom": "tool_connections", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_connections_application_id_tool_applications_id_fk": { + "name": "tool_connections_application_id_tool_applications_id_fk", + "tableFrom": "tool_connections", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tool_connections_created_by_agent_id_agents_id_fk": { + "name": "tool_connections_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_connections", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tool_connections_company_id_uq": { + "name": "tool_connections_company_id_uq", + "nullsNotDistinct": false, + "columns": [ + "company_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "tool_connections_ownership_check": { + "name": "tool_connections_ownership_check", + "value": "\"tool_connections\".\"ownership\" in ('platform_shared', 'platform_provisioned', 'customer', 'dcr')" + }, + "tool_connections_transport_check": { + "name": "tool_connections_transport_check", + "value": "\"tool_connections\".\"transport\" in ('mcp_remote', 'rest_api', 'local_stdio')" + }, + "tool_connections_auth_kind_check": { + "name": "tool_connections_auth_kind_check", + "value": "\"tool_connections\".\"auth_kind\" in ('oauth', 'api_key', 'none')" + } + }, + "isRLSEnabled": false + }, + "public.tool_gateway_rate_limit_counters": { + "name": "tool_gateway_rate_limit_counters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "counter_key": { + "name": "counter_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start_at": { + "name": "window_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_ms": { + "name": "window_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_at": { + "name": "reset_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": { + "tool_gateway_rate_limit_counters_company_idx": { + "name": "tool_gateway_rate_limit_counters_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_rate_limit_counters_window_uq": { + "name": "tool_gateway_rate_limit_counters_window_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "counter_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_gateway_rate_limit_counters_company_id_companies_id_fk": { + "name": "tool_gateway_rate_limit_counters_company_id_companies_id_fk", + "tableFrom": "tool_gateway_rate_limit_counters", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_gateway_sessions": { + "name": "tool_gateway_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_type": { + "name": "client_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_id": { + "name": "client_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_session_id": { + "name": "mcp_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_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": { + "tool_gateway_sessions_token_hash_uq": { + "name": "tool_gateway_sessions_token_hash_uq", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_company_agent_idx": { + "name": "tool_gateway_sessions_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_company_expires_idx": { + "name": "tool_gateway_sessions_company_expires_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_run_idx": { + "name": "tool_gateway_sessions_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_issue_idx": { + "name": "tool_gateway_sessions_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_gateway_sessions_gateway_idx": { + "name": "tool_gateway_sessions_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_gateway_sessions_company_id_companies_id_fk": { + "name": "tool_gateway_sessions_company_id_companies_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_gateway_sessions_agent_id_agents_id_fk": { + "name": "tool_gateway_sessions_agent_id_agents_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_gateway_sessions_run_id_heartbeat_runs_id_fk": { + "name": "tool_gateway_sessions_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_gateway_sessions_issue_id_issues_id_fk": { + "name": "tool_gateway_sessions_issue_id_issues_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_gateway_sessions_project_id_projects_id_fk": { + "name": "tool_gateway_sessions_project_id_projects_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_gateway_sessions_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_gateway_sessions_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_gateway_sessions_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_gateway_sessions_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_gateway_sessions", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_invocations": { + "name": "tool_invocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_token_id": { + "name": "gateway_token_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_type": { + "name": "client_subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_subject_id": { + "name": "client_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_session_id": { + "name": "mcp_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_version_hash": { + "name": "catalog_version_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "catalog_schema_hash": { + "name": "catalog_schema_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_key": { + "name": "application_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream_tool_name": { + "name": "upstream_tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "arguments_hash": { + "name": "arguments_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "arguments_summary": { + "name": "arguments_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "policy_decision": { + "name": "policy_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_policy_ids": { + "name": "matched_policy_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "policy_explanation": { + "name": "policy_explanation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_scope_summary": { + "name": "credential_scope_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "header_policy_summary": { + "name": "header_policy_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_required'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "upstream_request_id": { + "name": "upstream_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_hash": { + "name": "result_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_size_bytes": { + "name": "result_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "result_artifact_id": { + "name": "result_artifact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_invocations_company_created_idx": { + "name": "tool_invocations_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_run_idx": { + "name": "tool_invocations_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_issue_idx": { + "name": "tool_invocations_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_gateway_idx": { + "name": "tool_invocations_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_invocations_company_idempotency_uq": { + "name": "tool_invocations_company_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_invocations_company_id_companies_id_fk": { + "name": "tool_invocations_company_id_companies_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_invocations_agent_id_agents_id_fk": { + "name": "tool_invocations_agent_id_agents_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_issue_id_issues_id_fk": { + "name": "tool_invocations_issue_id_issues_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_run_id_heartbeat_runs_id_fk": { + "name": "tool_invocations_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_invocations_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_gateway_token_id_tool_mcp_gateway_tokens_id_fk": { + "name": "tool_invocations_gateway_token_id_tool_mcp_gateway_tokens_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_mcp_gateway_tokens", + "columnsFrom": [ + "gateway_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_application_id_tool_applications_id_fk": { + "name": "tool_invocations_application_id_tool_applications_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_connection_id_tool_connections_id_fk": { + "name": "tool_invocations_connection_id_tool_connections_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_invocations_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_invocations_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_invocations", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_mcp_gateway_tokens": { + "name": "tool_mcp_gateway_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "gateway_id": { + "name": "gateway_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gateway_client'" + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_label": { + "name": "client_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "owner_note": { + "name": "owner_note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "allowed_actions": { + "name": "allowed_actions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"tools/list\",\"tools/call\"]'::jsonb" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expiry_override_reason": { + "name": "expiry_override_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_override_by_user_id": { + "name": "expiry_override_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_override_by_agent_id": { + "name": "expiry_override_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expiry_override_at": { + "name": "expiry_override_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 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_mcp_gateway_tokens_token_hash_uq": { + "name": "tool_mcp_gateway_tokens_token_hash_uq", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateway_tokens_gateway_idx": { + "name": "tool_mcp_gateway_tokens_gateway_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gateway_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateway_tokens_subject_idx": { + "name": "tool_mcp_gateway_tokens_subject_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateway_tokens_company_expires_idx": { + "name": "tool_mcp_gateway_tokens_company_expires_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_mcp_gateway_tokens_company_id_companies_id_fk": { + "name": "tool_mcp_gateway_tokens_company_id_companies_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_mcp_gateway_tokens_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_mcp_gateway_tokens_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "tool_mcp_gateways", + "columnsFrom": [ + "gateway_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_mcp_gateway_tokens_expiry_override_by_agent_id_agents_id_fk": { + "name": "tool_mcp_gateway_tokens_expiry_override_by_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "agents", + "columnsFrom": [ + "expiry_override_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateway_tokens_created_by_agent_id_agents_id_fk": { + "name": "tool_mcp_gateway_tokens_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateway_tokens", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_mcp_gateways": { + "name": "tool_mcp_gateways", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "gateway_public_id": { + "name": "gateway_public_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gw_' || replace(gen_random_uuid()::text, '-', '')" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_slug": { + "name": "display_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "default_profile_mode": { + "name": "default_profile_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gateway_only'" + }, + "context_scope_type": { + "name": "context_scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "context_scope_id": { + "name": "context_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_issue_id": { + "name": "approval_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "auth_config": { + "name": "auth_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":1,\"bearer\":{\"enabled\":true,\"tokenPrefix\":\"pcgw\",\"defaultTtlSeconds\":7776000,\"requireFiniteExpiry\":true,\"longLivedTokenRequiresOverride\":true},\"oauth\":{\"enabled\":false,\"reservedFor\":\"v1_5\",\"dynamicClientRegistration\":false,\"authorizationCodePkce\":false}}'::jsonb" + }, + "header_policy": { + "name": "header_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":1,\"callerPassthrough\":{\"enabled\":false,\"allowedHeaders\":[]},\"staticHeaders\":[],\"generatedMetadata\":{\"enabled\":false,\"allowedHeaders\":[]},\"responseHeaders\":{\"forwardMcpRequiredHeaders\":true,\"forwardSafeCacheHeaders\":true}}'::jsonb" + }, + "metadata_policy": { + "name": "metadata_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"version\":1,\"forwardCompanyId\":false,\"forwardGatewayId\":false,\"forwardProjectId\":false,\"forwardIssueId\":false,\"forwardAgentId\":false,\"forwardRunId\":false,\"forwardCorrelationId\":true}'::jsonb" + }, + "on_demand_tools_config": { + "name": "on_demand_tools_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"enabled\":false,\"searchToolName\":\"search_tools\",\"runToolName\":\"run_tool\"}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_mcp_gateways_company_idx": { + "name": "tool_mcp_gateways_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_company_status_idx": { + "name": "tool_mcp_gateways_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_profile_idx": { + "name": "tool_mcp_gateways_profile_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_public_id_uq": { + "name": "tool_mcp_gateways_public_id_uq", + "columns": [ + { + "expression": "gateway_public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_company_slug_uq": { + "name": "tool_mcp_gateways_company_slug_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_mcp_gateways_company_name_uq": { + "name": "tool_mcp_gateways_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_mcp_gateways_company_id_companies_id_fk": { + "name": "tool_mcp_gateways_company_id_companies_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_mcp_gateways_profile_id_tool_profiles_id_fk": { + "name": "tool_mcp_gateways_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "tool_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "tool_mcp_gateways_agent_id_agents_id_fk": { + "name": "tool_mcp_gateways_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_project_id_projects_id_fk": { + "name": "tool_mcp_gateways_project_id_projects_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_issue_id_issues_id_fk": { + "name": "tool_mcp_gateways_issue_id_issues_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_approval_issue_id_issues_id_fk": { + "name": "tool_mcp_gateways_approval_issue_id_issues_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "issues", + "columnsFrom": [ + "approval_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_mcp_gateways_created_by_agent_id_agents_id_fk": { + "name": "tool_mcp_gateways_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateways", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_oauth_states": { + "name": "tool_oauth_states", + "schema": "", + "columns": { + "state": { + "name": "state", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_session_id": { + "name": "created_by_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_scopes": { + "name": "requested_scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "interaction_id": { + "name": "interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_oauth_states_company_idx": { + "name": "tool_oauth_states_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_oauth_states_connection_idx": { + "name": "tool_oauth_states_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_oauth_states_actor_idx": { + "name": "tool_oauth_states_actor_idx", + "columns": [ + { + "expression": "created_by_actor_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_oauth_states_expires_at_idx": { + "name": "tool_oauth_states_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_oauth_states_company_id_companies_id_fk": { + "name": "tool_oauth_states_company_id_companies_id_fk", + "tableFrom": "tool_oauth_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_oauth_states_connection_id_tool_connections_id_fk": { + "name": "tool_oauth_states_connection_id_tool_connections_id_fk", + "tableFrom": "tool_oauth_states", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_policies": { + "name": "tool_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_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 + }, + "policy_type": { + "name": "policy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "selectors": { + "name": "selectors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_policies_company_enabled_idx": { + "name": "tool_policies_company_enabled_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_policies_company_type_idx": { + "name": "tool_policies_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "policy_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_policies_company_name_uq": { + "name": "tool_policies_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_policies_company_id_companies_id_fk": { + "name": "tool_policies_company_id_companies_id_fk", + "tableFrom": "tool_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_policies_created_by_agent_id_agents_id_fk": { + "name": "tool_policies_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_policies", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_profile_bindings": { + "name": "tool_profile_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_profile_bindings_company_target_idx": { + "name": "tool_profile_bindings_company_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_bindings_target_profile_uq": { + "name": "tool_profile_bindings_target_profile_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_profile_bindings_company_id_companies_id_fk": { + "name": "tool_profile_bindings_company_id_companies_id_fk", + "tableFrom": "tool_profile_bindings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_bindings_profile_id_tool_profiles_id_fk": { + "name": "tool_profile_bindings_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_profile_bindings", + "tableTo": "tool_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_bindings_created_by_agent_id_agents_id_fk": { + "name": "tool_profile_bindings_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_profile_bindings", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_profile_entries": { + "name": "tool_profile_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selector_type": { + "name": "selector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'include'" + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "catalog_entry_id": { + "name": "catalog_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_level": { + "name": "risk_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "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": { + "tool_profile_entries_company_profile_idx": { + "name": "tool_profile_entries_company_profile_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_entries_application_idx": { + "name": "tool_profile_entries_application_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_entries_connection_idx": { + "name": "tool_profile_entries_connection_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profile_entries_catalog_entry_idx": { + "name": "tool_profile_entries_catalog_entry_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "catalog_entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_profile_entries_company_id_companies_id_fk": { + "name": "tool_profile_entries_company_id_companies_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_profile_id_tool_profiles_id_fk": { + "name": "tool_profile_entries_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_application_id_tool_applications_id_fk": { + "name": "tool_profile_entries_application_id_tool_applications_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_connection_id_tool_connections_id_fk": { + "name": "tool_profile_entries_connection_id_tool_connections_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_profile_entries_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_profile_entries_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_profile_entries", + "tableTo": "tool_catalog_entries", + "columnsFrom": [ + "catalog_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_profiles": { + "name": "tool_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "profile_key": { + "name": "profile_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "default_action": { + "name": "default_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'deny'" + }, + "new_tools_reviewed_at": { + "name": "new_tools_reviewed_at", + "type": "timestamp with time zone", + "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": { + "tool_profiles_company_status_idx": { + "name": "tool_profiles_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profiles_company_key_uq": { + "name": "tool_profiles_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_profiles_company_name_uq": { + "name": "tool_profiles_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_profiles_company_id_companies_id_fk": { + "name": "tool_profiles_company_id_companies_id_fk", + "tableFrom": "tool_profiles", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_rate_limit_counters": { + "name": "tool_rate_limit_counters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "counter_key": { + "name": "counter_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start_at": { + "name": "window_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reset_at": { + "name": "reset_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": { + "tool_rate_limit_counters_company_idx": { + "name": "tool_rate_limit_counters_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_rate_limit_counters_window_uq": { + "name": "tool_rate_limit_counters_window_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "counter_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_rate_limit_counters_company_id_companies_id_fk": { + "name": "tool_rate_limit_counters_company_id_companies_id_fk", + "tableFrom": "tool_rate_limit_counters", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_rate_limit_counters_policy_id_tool_policies_id_fk": { + "name": "tool_rate_limit_counters_policy_id_tool_policies_id_fk", + "tableFrom": "tool_rate_limit_counters", + "tableTo": "tool_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_runtime_metric_counters": { + "name": "tool_runtime_metric_counters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket_start_at": { + "name": "bucket_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "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": { + "tool_runtime_metric_counters_company_metric_idx": { + "name": "tool_runtime_metric_counters_company_metric_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bucket_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_metric_counters_bucket_uq": { + "name": "tool_runtime_metric_counters_bucket_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bucket_start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_runtime_metric_counters_company_id_companies_id_fk": { + "name": "tool_runtime_metric_counters_company_id_companies_id_fk", + "tableFrom": "tool_runtime_metric_counters", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_runtime_slots": { + "name": "tool_runtime_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_scope_type": { + "name": "owner_scope_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connection'" + }, + "owner_scope_id": { + "name": "owner_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_kind": { + "name": "runtime_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_stdio'" + }, + "slot_key": { + "name": "slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stopped'" + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_scope": { + "name": "workspace_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_scope_hash": { + "name": "credential_scope_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_id": { + "name": "process_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "command_template_key": { + "name": "command_template_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unchecked'" + }, + "health_message": { + "name": "health_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_health_check_at": { + "name": "last_health_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stopped_at": { + "name": "stopped_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 + }, + "idle_expires_at": { + "name": "idle_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "idle_deadline_at": { + "name": "idle_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "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": { + "tool_runtime_slots_company_idx": { + "name": "tool_runtime_slots_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_slots_connection_idx": { + "name": "tool_runtime_slots_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_slots_execution_workspace_idx": { + "name": "tool_runtime_slots_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_runtime_slots_slot_key_uq": { + "name": "tool_runtime_slots_slot_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slot_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_runtime_slots_company_id_companies_id_fk": { + "name": "tool_runtime_slots_company_id_companies_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_runtime_slots_application_id_tool_applications_id_fk": { + "name": "tool_runtime_slots_application_id_tool_applications_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "tool_applications", + "columnsFrom": [ + "application_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_runtime_slots_connection_id_tool_connections_id_fk": { + "name": "tool_runtime_slots_connection_id_tool_connections_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "tool_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_runtime_slots_project_workspace_id_project_workspaces_id_fk": { + "name": "tool_runtime_slots_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_runtime_slots_execution_workspace_id_execution_workspaces_id_fk": { + "name": "tool_runtime_slots_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tool_runtime_slots_issue_id_issues_id_fk": { + "name": "tool_runtime_slots_issue_id_issues_id_fk", + "tableFrom": "tool_runtime_slots", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_stdio_command_templates": { + "name": "tool_stdio_command_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "env_keys": { + "name": "env_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_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": { + "tool_stdio_command_templates_company_idx": { + "name": "tool_stdio_command_templates_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_stdio_command_templates_company_status_idx": { + "name": "tool_stdio_command_templates_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_stdio_command_templates_company_key_uq": { + "name": "tool_stdio_command_templates_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tool_stdio_command_templates_company_id_companies_id_fk": { + "name": "tool_stdio_command_templates_company_id_companies_id_fk", + "tableFrom": "tool_stdio_command_templates", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tool_stdio_command_templates_created_by_agent_id_agents_id_fk": { + "name": "tool_stdio_command_templates_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_stdio_command_templates", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_inbox_agent_policies": { + "name": "user_inbox_agent_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "allowed_agent_ids": { + "name": "allowed_agent_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_inbox_agent_policies_company_user_uq": { + "name": "user_inbox_agent_policies_company_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_inbox_agent_policies_allowed_agent_ids_idx": { + "name": "user_inbox_agent_policies_allowed_agent_ids_idx", + "columns": [ + { + "expression": "allowed_agent_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "user_inbox_agent_policies_company_id_companies_id_fk": { + "name": "user_inbox_agent_policies_company_id_companies_id_fk", + "tableFrom": "user_inbox_agent_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_inbox_agent_policies_mode_check": { + "name": "user_inbox_agent_policies_mode_check", + "value": "\"user_inbox_agent_policies\".\"mode\" in ('open', 'allowlist', 'disabled')" + } + }, + "isRLSEnabled": false + }, + "public.user_secret_declarations": { + "name": "user_secret_declarations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_secret_definition_id": { + "name": "user_secret_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_path": { + "name": "config_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_selector": { + "name": "version_selector", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'latest'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_missing_override": { + "name": "allow_missing_override", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "label": { + "name": "label", + "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": { + "user_secret_declarations_company_idx": { + "name": "user_secret_declarations_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_definition_idx": { + "name": "user_secret_declarations_definition_idx", + "columns": [ + { + "expression": "user_secret_definition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_target_idx": { + "name": "user_secret_declarations_target_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_company_required_idx": { + "name": "user_secret_declarations_company_required_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "required", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_target_path_uq": { + "name": "user_secret_declarations_target_path_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_declarations_required_override_idx": { + "name": "user_secret_declarations_required_override_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "allow_missing_override", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_secret_declarations\".\"allow_missing_override\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_secret_declarations_company_id_companies_id_fk": { + "name": "user_secret_declarations_company_id_companies_id_fk", + "tableFrom": "user_secret_declarations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk": { + "name": "user_secret_declarations_user_secret_definition_id_user_secret_definitions_id_fk", + "tableFrom": "user_secret_declarations", + "tableTo": "user_secret_definitions", + "columnsFrom": [ + "user_secret_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_secret_definitions": { + "name": "user_secret_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "managed_mode": { + "name": "managed_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip_managed'" + }, + "provider_config_id": { + "name": "provider_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "usage_guidance": { + "name": "usage_guidance", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "user_secret_definitions_company_status_idx": { + "name": "user_secret_definitions_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_definitions_company_provider_idx": { + "name": "user_secret_definitions_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_definitions_provider_config_idx": { + "name": "user_secret_definitions_provider_config_idx", + "columns": [ + { + "expression": "provider_config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_secret_definitions_company_key_uq": { + "name": "user_secret_definitions_company_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_secret_definitions\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_secret_definitions_company_id_companies_id_fk": { + "name": "user_secret_definitions_company_id_companies_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk": { + "name": "user_secret_definitions_provider_config_id_company_secret_provider_configs_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "company_secret_provider_configs", + "columnsFrom": [ + "provider_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_secret_definitions_created_by_agent_id_agents_id_fk": { + "name": "user_secret_definitions_created_by_agent_id_agents_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_secret_definitions_updated_by_agent_id_agents_id_fk": { + "name": "user_secret_definitions_updated_by_agent_id_agents_id_fk", + "tableFrom": "user_secret_definitions", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sidebar_preferences": { + "name": "user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_order": { + "name": "company_order", + "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": { + "user_sidebar_preferences_user_uq": { + "name": "user_sidebar_preferences_user_uq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operations": { + "name": "workspace_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_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": { + "workspace_operations_company_run_started_idx": { + "name": "workspace_operations_company_run_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_started_idx": { + "name": "workspace_operations_company_workspace_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_issue_started_idx": { + "name": "workspace_operations_company_workspace_issue_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_issue_id_issues_id_fk": { + "name": "workspace_operations_issue_id_issues_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_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": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exposure_handle": { + "name": "exposure_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backend_url": { + "name": "backend_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_execution_workspace_status_idx": { + "name": "workspace_runtime_services_company_execution_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_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 ee901e2db3..6abe702c5e 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1534,6 +1534,20 @@ "when": 1786711898731, "tag": "0220_execution_workspace_runtime_leases", "breakpoints": true + }, + { + "idx": 221, + "version": "7", + "when": 1786983881320, + "tag": "0221_swift_justin_hammer", + "breakpoints": true + }, + { + "idx": 222, + "version": "7", + "when": 1786983999395, + "tag": "0222_orphan_cleanup_env_reference_survives_delete", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/claude_setup_token_sessions.ts b/packages/db/src/schema/claude_setup_token_sessions.ts new file mode 100644 index 0000000000..cc32e08f3d --- /dev/null +++ b/packages/db/src/schema/claude_setup_token_sessions.ts @@ -0,0 +1,83 @@ +import { sql } from "drizzle-orm"; +import { index, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; +import type { AgentAdapterType } from "@paperclipai/shared"; +import { companies } from "./companies.js"; +import { environments } from "./environments.js"; + +/** + * The durable state of one Claude setup-token cleanup record. The store never + * reuses `adapter_auth_sessions`; this table is a dedicated, non-secret cleanup + * store. The active states hold a live login. The `stored` state marks a session + * that wrote the owner-bound secret and now persists as a one-time claim. The + * create path consumes the claim by setting `bound_at`. The reaper removes the + * row after `deadline_at`. The terminal states end the login. + */ +export type ClaudeSetupTokenSessionState = + | "starting" + | "awaiting_code" + | "submitting" + | "persisting" + | "stored" + | "completed" + | "failed" + | "timed_out" + | "cancelled"; + +// The active states hold the company credential slot. The partial unique index +// applies to these states only. It does not include `stored` or a terminal +// state, so a stored claim and a terminal record do not block a new login. +export const CLAUDE_SETUP_TOKEN_ACTIVE_STATES = [ + "starting", + "awaiting_code", + "submitting", + "persisting", +] as const satisfies readonly ClaudeSetupTokenSessionState[]; + +// The durable store for one Claude setup-token cleanup record. One row tracks +// one login for one owner, one adapter, and one environment. The row keeps only +// ids, the state, the sandbox lease reference, and the deadlines. The row never +// holds the login URL, the browser code, the token, or a process chunk. +export const claudeSetupTokenSessions = pgTable( + "claude_setup_token_sessions", + { + id: uuid("id").primaryKey().defaultRandom(), + // The opaque service session id. The store keys every update by this id. + sessionId: text("session_id").notNull(), + companyId: uuid("company_id") + .notNull() + .references(() => companies.id, { onDelete: "cascade" }), + // The immutable owner principal. The service sets this column one time at + // create and never updates it. + ownerUserId: text("owner_user_id").notNull(), + adapterType: text("adapter_type").$type().notNull(), + environmentId: uuid("environment_id") + .notNull() + .references(() => environments.id, { onDelete: "cascade" }), + // The provider lease reference for the sandbox. The reaper reads it to + // release a lease that a crash or a failure left behind. It is not a secret. + leaseId: text("lease_id"), + state: text("state").$type().notNull().default("starting"), + // The login deadline. The reaper removes the row after this time. The + // claim-consumption write also checks this column with `clock_timestamp()`. + deadlineAt: timestamp("deadline_at", { withTimezone: true }).notNull(), + // The claim-consumption marker. It is null while the stored claim is live. + // The create path sets it one time when it consumes the claim. A non-null + // value marks a consumed claim, so the reaper removes the row. + boundAt: timestamp("bound_at", { withTimezone: true }), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + // Serialize the active login on the full owner scope. Only one active + // session can run per company, owner, adapter, and environment. The index + // applies to the active states only; it excludes `stored` and every terminal + // state, so a stored claim and a terminal record never block a new login. + activeUq: uniqueIndex("claude_setup_token_sessions_active_uq") + .on(table.companyId, table.ownerUserId, table.adapterType, table.environmentId) + .where(sql`${table.state} IN ('starting', 'awaiting_code', 'submitting', 'persisting')`), + // The store keys every update by the opaque session id, so it stays unique. + sessionIdUq: uniqueIndex("claude_setup_token_sessions_session_id_uq").on(table.sessionId), + // The reaper scans by the deadline to remove an expired record. + deadlineIdx: index("claude_setup_token_sessions_deadline_idx").on(table.deadlineAt), + }), +); diff --git a/packages/db/src/schema/environment_leases.ts b/packages/db/src/schema/environment_leases.ts index ea01db2bd6..87819eabc7 100644 --- a/packages/db/src/schema/environment_leases.ts +++ b/packages/db/src/schema/environment_leases.ts @@ -10,7 +10,14 @@ export const environmentLeases = pgTable( { id: uuid("id").primaryKey().defaultRandom(), companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }), - environmentId: uuid("environment_id").notNull().references(() => environments.id, { onDelete: "cascade" }), + // A lease-less orphan sandbox is recorded here as a `pending_cleanup` row. + // That row is the only durable teardown reference the cleanup sweep has for + // the remote sandbox. So the environment reference must survive an + // environment delete: `on delete set null` keeps the row, and the sweep + // tears the orphan down from the row's immutable provider metadata. The + // column is nullable so an orphan the acquire records after a concurrent + // environment delete still persists with a null reference. + environmentId: uuid("environment_id").references(() => environments.id, { onDelete: "set null" }), executionWorkspaceId: uuid("execution_workspace_id").references(() => executionWorkspaces.id, { onDelete: "set null" }), issueId: uuid("issue_id").references(() => issues.id, { onDelete: "set null" }), heartbeatRunId: uuid("heartbeat_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }), diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index ad878e2e83..16fd35c306 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -35,6 +35,11 @@ export { environmentLeases } from "./environment_leases.js"; export { environmentCustomImageTemplates } from "./environment_custom_image_templates.js"; export { environmentCustomImageSetupSessions } from "./environment_custom_image_setup_sessions.js"; export { adapterAuthSessions } from "./adapter_auth_sessions.js"; +export { + claudeSetupTokenSessions, + CLAUDE_SETUP_TOKEN_ACTIVE_STATES, + type ClaudeSetupTokenSessionState, +} from "./claude_setup_token_sessions.js"; export { workspaceOperations } from "./workspace_operations.js"; export { workspaceRuntimeServices } from "./workspace_runtime_services.js"; export { projectGoals } from "./project_goals.js"; diff --git a/packages/plugins/sandbox-providers/daytona/src/manifest.ts b/packages/plugins/sandbox-providers/daytona/src/manifest.ts index e4b70895d3..a7e19fdc90 100644 --- a/packages/plugins/sandbox-providers/daytona/src/manifest.ts +++ b/packages/plugins/sandbox-providers/daytona/src/manifest.ts @@ -1,7 +1,10 @@ import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk"; const PLUGIN_ID = "paperclip.daytona-sandbox-provider"; -const PLUGIN_VERSION = "0.1.1"; +// 0.1.2 adds `supportsSetupTokenLogin` to the driver. The version bump makes +// the bundled-plugin boot reconcile refresh the persisted manifest for an +// existing install, so the Claude setup-token login capability propagates. +const PLUGIN_VERSION = "0.1.2"; const manifest: PaperclipPluginManifestV1 = { id: PLUGIN_ID, @@ -41,6 +44,10 @@ const manifest: PaperclipPluginManifestV1 = { }, templateIdentityPaths: ["apiUrl"], supportsTemplateDelete: true, + // Daytona hosts the Claude setup-token login on a real pseudo-terminal. + // It is the only bundled provider that implements the setup-token + // pseudo-terminal methods, so it advertises the capability. + supportsSetupTokenLogin: true, configSchema: { type: "object", properties: { diff --git a/packages/plugins/sandbox-providers/daytona/src/plugin.test.ts b/packages/plugins/sandbox-providers/daytona/src/plugin.test.ts index 75bbc8482e..e6c85b06a4 100644 --- a/packages/plugins/sandbox-providers/daytona/src/plugin.test.ts +++ b/packages/plugins/sandbox-providers/daytona/src/plugin.test.ts @@ -45,6 +45,7 @@ function createMockSandbox(overrides: { state?: string; recoverable?: boolean; workDir?: string; + autoDestroyAt?: string | null; } = {}) { return { id: overrides.id ?? "sandbox-123", @@ -53,6 +54,9 @@ function createMockSandbox(overrides: { recoverable: overrides.recoverable ?? false, target: "us", errorReason: null, + // A configured provider TTL populates `autoDestroyAt` after `setTtl` + + // `refreshData`. The default mock leaves it unset (no TTL configured). + autoDestroyAt: overrides.autoDestroyAt ?? undefined, getWorkDir: vi.fn().mockResolvedValue(overrides.workDir ?? "/home/daytona"), getUserHomeDir: vi.fn().mockResolvedValue("/home/daytona"), start: vi.fn().mockResolvedValue(undefined), @@ -65,6 +69,7 @@ function createMockSandbox(overrides: { resize: vi.fn().mockResolvedValue(undefined), delete: vi.fn().mockResolvedValue(undefined), archive: vi.fn().mockResolvedValue(undefined), + setTtl: vi.fn().mockResolvedValue(undefined), setAutoDeleteInterval: vi.fn().mockResolvedValue(undefined), createSshAccess: vi.fn().mockResolvedValue({ token: "ssh-token-secret", @@ -344,6 +349,71 @@ describe("Daytona sandbox provider plugin", () => { ); }); + it("does not configure a provider ttl when the acquire carries no requested expiry", async () => { + process.env.DAYTONA_API_KEY = "host-key"; + const sandbox = createMockSandbox(); + mockCreate.mockResolvedValue(sandbox); + + const lease = await plugin.definition.onEnvironmentAcquireLease?.({ + driverKey: "daytona", + companyId: "company-1", + environmentId: "env-1", + runId: "run-1", + config: { image: "node:20", timeoutMs: 300000, reuseLease: false }, + }); + + // A generic caller keeps the current behavior: no provider ttl, no expiry. + expect(sandbox.setTtl).not.toHaveBeenCalled(); + expect(lease?.expiresAt ?? null).toBeNull(); + }); + + it("configures a provider ttl at or before the requested expiry and returns the provider expiry", async () => { + process.env.DAYTONA_API_KEY = "host-key"; + const autoDestroyAt = new Date(Date.now() + 30 * 60_000).toISOString(); + const sandbox = createMockSandbox({ autoDestroyAt }); + mockCreate.mockResolvedValue(sandbox); + + const requestedExpiresAt = new Date(Date.now() + 30 * 60_000 + 30_000).toISOString(); + const lease = await plugin.definition.onEnvironmentAcquireLease?.({ + driverKey: "daytona", + companyId: "company-1", + environmentId: "env-1", + runId: "run-1", + config: { image: "node:20", timeoutMs: 300000, reuseLease: false }, + requestedExpiresAt, + }); + + // The provider ttl is rounded DOWN to whole minutes, so the destroy time + // never lands after the requested deadline. + expect(sandbox.setTtl).toHaveBeenCalledTimes(1); + expect(sandbox.setTtl).toHaveBeenCalledWith(30); + expect(sandbox.refreshData).toHaveBeenCalled(); + // The lease carries the real provider destroy time as evidence of the bound. + expect(lease?.expiresAt).toBe(autoDestroyAt); + }); + + it("returns no expiry when the requested deadline is less than one minute away", async () => { + process.env.DAYTONA_API_KEY = "host-key"; + const sandbox = createMockSandbox({ autoDestroyAt: "must-not-be-read" }); + mockCreate.mockResolvedValue(sandbox); + + const requestedExpiresAt = new Date(Date.now() + 30_000).toISOString(); + const lease = await plugin.definition.onEnvironmentAcquireLease?.({ + driverKey: "daytona", + companyId: "company-1", + environmentId: "env-1", + runId: "run-1", + config: { image: "node:20", timeoutMs: 300000, reuseLease: false }, + requestedExpiresAt, + }); + + // Daytona ttl granularity is one minute, so a nearer deadline maps to no + // valid provider ttl. The provider grants no expiry and the server fails + // closed on the null expiry. + expect(sandbox.setTtl).not.toHaveBeenCalled(); + expect(lease?.expiresAt ?? null).toBeNull(); + }); + it("starts an interactive setup sandbox with redacted metadata and one-time SSH payload", async () => { process.env.DAYTONA_API_KEY = "host-key"; const sandbox = createMockSandbox(); diff --git a/packages/plugins/sandbox-providers/daytona/src/plugin.ts b/packages/plugins/sandbox-providers/daytona/src/plugin.ts index 82a15a73f0..2416b17fcd 100644 --- a/packages/plugins/sandbox-providers/daytona/src/plugin.ts +++ b/packages/plugins/sandbox-providers/daytona/src/plugin.ts @@ -60,6 +60,11 @@ export type { DaytonaPtyCreateOptions, DaytonaSetupTokenPtyOptions, } from "./setup-token-pty.js"; +import { openDaytonaSetupTokenPtySession as openSetupTokenPtySession } from "./setup-token-pty.js"; +import type { + SetupTokenPtySession as SetupTokenPtyWorkerSession, + DaytonaPtyProcess, +} from "./setup-token-pty.js"; // Injectable monotonic clock for provider-boundary timing (Open Q1). Defaults // to the real wall clock; `plugin.test.ts` overrides it via @@ -628,6 +633,33 @@ function expiresAtForMinutes(minutes: number): string { return new Date(Date.now() + minutes * 60_000).toISOString(); } +// Configure a provider-side time-to-live so Daytona destroys the sandbox at or +// before the caller-requested deadline, even after a Paperclip crash or outage. +// `setTtl` counts wall-clock time regardless of the sandbox state, so the destroy +// happens even when the sandbox is stopped, paused, or archived. The function +// returns the real provider destroy time (`autoDestroyAt`) as evidence of the +// provider-side bound. It returns null when the caller sets no deadline, when the +// deadline is invalid, or when the deadline is less than one minute away (Daytona +// TTL granularity is one minute, so a nearer deadline maps to no valid TTL). The +// server then fails closed on a null expiry and releases the lease. +async function configureSandboxExpiry(input: { + sandbox: Sandbox; + requestedExpiresAt: string | null | undefined; + nowMs: number; +}): Promise { + const requestedMs = input.requestedExpiresAt ? Date.parse(input.requestedExpiresAt) : Number.NaN; + if (!Number.isFinite(requestedMs)) return null; + // Round DOWN so the provider destroy time never lands after the deadline. + const ttlMinutes = Math.floor((requestedMs - input.nowMs) / 60_000); + if (ttlMinutes < 1) return null; + await input.sandbox.setTtl(ttlMinutes); + await input.sandbox.refreshData(); + const autoDestroyAt = input.sandbox.autoDestroyAt; + return typeof autoDestroyAt === "string" && autoDestroyAt.trim().length > 0 + ? autoDestroyAt.trim() + : null; +} + function sanitizeSnapshotName(value: string | null | undefined, fallback: string): string { const cleaned = (value ?? fallback) .trim() @@ -1224,7 +1256,27 @@ const sandboxHandleCache = (() => { entries.clear(); } - return { get, seed, clear, reset, markFresh }; + // Resolve a cached sandbox by its provider lease id alone. The + // login pseudo-terminal open carries only the provider lease id, not the full + // scope, so this scans the cached handles for the one whose `sandbox.id` + // matches. The lease was cached on acquire in the same worker, so the scan is + // a hit for a live login lease. It returns null when no cached handle matches, + // so the caller fails closed. + async function findByProviderLeaseId(providerLeaseId: string): Promise { + if (!providerLeaseId) return null; + for (const entry of entries.values()) { + let sandbox: Sandbox; + try { + sandbox = await entry.sandbox; + } catch { + continue; + } + if (sandbox.id === providerLeaseId) return sandbox; + } + return null; + } + + return { get, seed, clear, reset, markFresh, findByProviderLeaseId }; })(); // Advisory writable-set store. It holds, per lease scope, the sandbox @@ -1829,6 +1881,24 @@ async function executeInSession( } } +// The worker-side registry of live login pseudo-terminal sessions. +// The worker registers each terminal under the host-owned route identifier at +// create time, so the host closes the exact terminal by that identifier even +// when the open reply was lost. It also indexes by the worker session identifier +// for input and stop. The `onShutdown` hook closes every open session here. +interface DaytonaSetupTokenPtyEntry { + hostRouteId: string; + workerSessionId: string; + session: SetupTokenPtyWorkerSession; +} +const daytonaSetupTokenPtyByRoute = new Map(); +const daytonaSetupTokenPtyBySession = new Map(); + +function forgetDaytonaSetupTokenPty(entry: DaytonaSetupTokenPtyEntry): void { + daytonaSetupTokenPtyByRoute.delete(entry.hostRouteId); + daytonaSetupTokenPtyBySession.delete(entry.workerSessionId); +} + const plugin = definePlugin({ async setup(ctx) { // Hoist the context to a module variable so the lifecycle hooks and the @@ -1956,6 +2026,14 @@ const plugin = definePlugin({ try { const remoteCwd = await resolveSandboxWorkingDirectory(sandbox); const shellCommand = await detectSandboxShellCommand(sandbox, toTimeoutSeconds(config.timeoutMs)); + // Configure a provider-side destroy time at or before a caller deadline, so + // an abandoned sandbox self-destroys even if Paperclip is down. The lease + // carries the real provider expiry (or none) as evidence of the bound. + const expiresAt = await configureSandboxExpiry({ + sandbox, + requestedExpiresAt: params.requestedExpiresAt, + nowMs: Date.now(), + }); const workspaceSentinel = await writeWorkspaceSentinel({ sandbox, remoteCwd, @@ -1985,6 +2063,7 @@ const plugin = definePlugin({ ); return { providerLeaseId: sandbox.id, + expiresAt, metadata: leaseMetadata({ config, sandbox, @@ -2588,6 +2667,84 @@ const plugin = definePlugin({ return result; }); }, + + // Open one live Claude `setup-token` login pseudo-terminal. Resolve + // the cached sandbox by the provider lease id, run the fixed login command on a + // real pseudo-terminal, and register the session under the host route id. Stream + // the raw output and the exit through `ctx.setupTokenPty`, bound to the returned + // worker session id. Fail closed when no cached sandbox matches the lease. + async onSetupTokenPtyOpen(params) { + const sandbox = await sandboxHandleCache.findByProviderLeaseId(params.providerLeaseId); + if (!sandbox) { + throw new Error( + "Daytona setup-token login: no cached sandbox resolves the provider lease.", + ); + } + const session = await openSetupTokenPtySession( + sandbox.process as unknown as DaytonaPtyProcess, + params.command, + ); + const workerSessionId = `pty-${randomUUID()}`; + const entry: DaytonaSetupTokenPtyEntry = { + hostRouteId: params.hostRouteId, + workerSessionId, + session, + }; + daytonaSetupTokenPtyByRoute.set(params.hostRouteId, entry); + daytonaSetupTokenPtyBySession.set(workerSessionId, entry); + // Register the output listener before the first input, so no early output + // chunk is lost. The client stamps the worker session id, so the host binds + // the output to the open route. + session.onData((chunk) => { + pluginContext?.setupTokenPty.output(workerSessionId, chunk); + }); + // Forward the child exit one time. The host resolves the login run on it. + void session.wait().then( + (result) => pluginContext?.setupTokenPty.exit(workerSessionId, result.exitCode), + () => pluginContext?.setupTokenPty.exit(workerSessionId, null), + ); + return { workerSessionId }; + }, + + // Write delayed input to an open login pseudo-terminal, keyed by the worker + // session id. Drop the input for an unknown session. + async onSetupTokenPtyInput(params) { + const entry = daytonaSetupTokenPtyBySession.get(params.workerSessionId); + if (!entry) return; + entry.session.write(params.data); + }, + + // Stop an open login pseudo-terminal child, keyed by the worker session id. + async onSetupTokenPtyStop(params) { + const entry = daytonaSetupTokenPtyBySession.get(params.workerSessionId); + if (!entry) return; + entry.session.kill(); + }, + + // Close an open login pseudo-terminal by the host route id and acknowledge the + // close with the same identifier. The close is idempotent: it returns the + // acknowledgement even when the entry is already gone, so the host confirms the + // terminal is closed. The worker never keys the close on the worker session id. + async onSetupTokenPtyClose(params) { + const entry = daytonaSetupTokenPtyByRoute.get(params.hostRouteId); + if (entry) { + forgetDaytonaSetupTokenPty(entry); + await entry.session.close().catch(() => undefined); + } + return { hostRouteId: params.hostRouteId }; + }, + + // Close every open login pseudo-terminal on an orderly shutdown, then drain the + // sandbox handle cache, so a graceful shutdown holds no live login terminal. + async onShutdown() { + const openSessions = [...daytonaSetupTokenPtyByRoute.values()]; + daytonaSetupTokenPtyByRoute.clear(); + daytonaSetupTokenPtyBySession.clear(); + for (const entry of openSessions) { + await entry.session.close().catch(() => undefined); + } + sandboxHandleCache.reset(); + }, }); export default plugin; diff --git a/packages/plugins/sdk/src/define-plugin.ts b/packages/plugins/sdk/src/define-plugin.ts index d2fc523222..c8af779839 100644 --- a/packages/plugins/sdk/src/define-plugin.ts +++ b/packages/plugins/sdk/src/define-plugin.ts @@ -82,6 +82,12 @@ import type { PluginExternalObjectResolveResult, RefreshExternalObjectsParams, RefreshExternalObjectsResult, + PluginSetupTokenPtyOpenParams, + PluginSetupTokenPtyOpenResult, + PluginSetupTokenPtyInputParams, + PluginSetupTokenPtyStopParams, + PluginSetupTokenPtyCloseParams, + PluginSetupTokenPtyCloseResult, } from "./protocol.js"; // --------------------------------------------------------------------------- @@ -432,6 +438,32 @@ export interface PluginDefinition { onEnvironmentDeleteTemplate?( params: PluginEnvironmentDeleteTemplateParams, ): Promise; + + /** + * Called to open one live Claude `setup-token` login pseudo-terminal. + * The worker registers the terminal under the host route identifier and returns a + * worker session identifier for the output notification binding only. The worker + * streams output and the exit through `ctx.setupTokenPty`, never as a reply. + * Defining the four `onSetupTokenPty*` hooks advertises the four methods. + */ + onSetupTokenPtyOpen?( + params: PluginSetupTokenPtyOpenParams, + ): Promise; + + /** Called to write delayed input to an open login pseudo-terminal, keyed by the worker session identifier. */ + onSetupTokenPtyInput?(params: PluginSetupTokenPtyInputParams): Promise; + + /** Called to stop an open login pseudo-terminal child, keyed by the worker session identifier. */ + onSetupTokenPtyStop?(params: PluginSetupTokenPtyStopParams): Promise; + + /** + * Called to close an open login pseudo-terminal by the host route identifier. The + * worker closes the exact terminal registered under that identifier and returns a + * close acknowledgement that carries the same identifier. + */ + onSetupTokenPtyClose?( + params: PluginSetupTokenPtyCloseParams, + ): Promise; } // --------------------------------------------------------------------------- diff --git a/packages/plugins/sdk/src/index.ts b/packages/plugins/sdk/src/index.ts index c8f8f4d406..c34b1a4d24 100644 --- a/packages/plugins/sdk/src/index.ts +++ b/packages/plugins/sdk/src/index.ts @@ -82,6 +82,8 @@ export { parseMessage, JsonRpcParseError, JsonRpcCallError, + SETUP_TOKEN_PTY_OUTPUT_NOTIFICATION, + SETUP_TOKEN_PTY_EXIT_NOTIFICATION, _resetIdCounter, } from "./protocol.js"; diff --git a/packages/plugins/sdk/src/protocol.ts b/packages/plugins/sdk/src/protocol.ts index 3fb8e8035a..519770768d 100644 --- a/packages/plugins/sdk/src/protocol.ts +++ b/packages/plugins/sdk/src/protocol.ts @@ -628,6 +628,17 @@ export interface PluginEnvironmentAcquireLeaseParams extends PluginEnvironmentDr */ adapterType?: string; executionWorkspaceSettings?: Record | null; + /** + * The absolute latest time the acquired lease may stay active, as an ISO 8601 + * timestamp. A caller with an independent deadline (for example the setup-token + * login session) sets it. A provider that materializes a sandbox must configure + * a provider-side expiry at or before this time, and return the real provider + * expiry in `PluginEnvironmentLease.expiresAt`. When the provider cannot bound + * the sandbox at or before this time, it returns no expiry, so the server fails + * closed and releases the lease. When omitted, the provider keeps its default + * lifetime. + */ + requestedExpiresAt?: string | null; } export interface PluginEnvironmentResumeLeaseParams extends PluginEnvironmentDriverBaseParams { @@ -960,6 +971,117 @@ export interface PluginRenderCloseEvent { nativeEvent?: unknown; } +// --------------------------------------------------------------------------- +// Setup-token login pseudo-terminal (PTY) worker methods. +// --------------------------------------------------------------------------- +// The host drives one live Claude `setup-token` login pseudo-terminal inside a +// sandbox provider worker. The host owns the route. It mints an opaque host +// route identifier, carries that identifier in the open request, and keys the +// close on that identifier. The worker registers the terminal under the host +// route identifier and returns a worker session identifier for the output +// notification binding only. The worker never keys a close on the worker +// session identifier, so the host closes a worker-created terminal even when the +// open reply was lost and no worker session identifier arrived. The worker sends +// output and exit as notifications, never as a reply, so the host binds them by +// the worker session identifier while the route is open. + +/** The open request for one live login pseudo-terminal. The worker registers the terminal by `hostRouteId`. */ +export interface PluginSetupTokenPtyOpenParams { + /** The host-owned opaque route identifier. The worker registers the terminal by it. */ + hostRouteId: string; + /** The environment driver key, for the worker sandbox scope. */ + driverKey: string; + /** The company that owns the login session. */ + companyId: string; + /** The environment the login session runs in. */ + environmentId: string; + /** The provider lease the sandbox is cached under. The worker resolves the sandbox by it. */ + providerLeaseId: string; + /** The fixed login command. The worker runs only this command on the terminal. */ + command: string; +} + +/** The open reply. It returns the worker session identifier for output binding only. */ +export interface PluginSetupTokenPtyOpenResult { + /** The worker session identifier. It binds the output and the exit notification only. */ + workerSessionId: string; +} + +/** The input request. It carries the worker session identifier and the raw input bytes. */ +export interface PluginSetupTokenPtyInputParams { + /** The worker session identifier that the open reply returned. */ + workerSessionId: string; + /** The raw input bytes to write to the terminal. */ + data: string; +} + +/** The stop request. It carries the worker session identifier. */ +export interface PluginSetupTokenPtyStopParams { + /** The worker session identifier that the open reply returned. */ + workerSessionId: string; +} + +/** The close request. The host route identifier is the authoritative key. */ +export interface PluginSetupTokenPtyCloseParams { + /** + * The host-owned opaque route identifier. This is the authoritative close key, + * so the host closes the terminal even when no worker session identifier + * arrived after a lost open reply. + */ + hostRouteId: string; + /** + * A non-authoritative worker session identifier. The worker never keys the + * close on it. The field is optional, so a close with only the host route + * identifier is a valid request for this lifecycle. + */ + workerSessionId?: string; +} + +/** The close reply. It acknowledges the close and carries the same host route identifier. */ +export interface PluginSetupTokenPtyCloseResult { + /** The close acknowledgement. It carries the same host route identifier the close sent. */ + hostRouteId: string; +} + +/** The worker→host pseudo-terminal output notification parameters. Modeled on `execute.log`. */ +export interface PluginSetupTokenPtyOutputParams { + /** The worker session identifier that the open reply returned. */ + workerSessionId: string; + /** The raw terminal output bytes. */ + chunk: string; +} + +/** The worker→host pseudo-terminal exit notification parameters. */ +export interface PluginSetupTokenPtyExitParams { + /** The worker session identifier that the open reply returned. */ + workerSessionId: string; + /** The child exit code, or null when the child ended with no code. */ + exitCode: number | null; +} + +/** + * One live login pseudo-terminal session in the worker. The worker opener returns + * it. The shape matches the sandbox provider setup-token pseudo-terminal session, + * so a provider passes its session with no adapter. + */ +export interface PluginSetupTokenPtyWorkerSession { + /** Registers the one output listener. The session streams each raw chunk in order. */ + onData(listener: (chunk: string) => void): void; + /** Writes raw input bytes to the pseudo-terminal. */ + write(data: string): void; + /** Resolves with the child exit code when the command ends. */ + wait(): Promise<{ exitCode: number | null }>; + /** Stops the child process. Safe to call more than one time. */ + kill(): void; + /** Releases the session resources. Safe to call more than one time. */ + close(): Promise; +} + +/** The worker→host notification method for one pseudo-terminal output chunk. */ +export const SETUP_TOKEN_PTY_OUTPUT_NOTIFICATION = "setupTokenPty.output"; +/** The worker→host notification method for one pseudo-terminal exit. */ +export const SETUP_TOKEN_PTY_EXIT_NOTIFICATION = "setupTokenPty.exit"; + /** * Map of host→worker RPC method names to their `[params, result]` types. * @@ -1064,6 +1186,20 @@ export interface HostToWorkerMethods { params: PluginEnvironmentDeleteTemplateParams, result: PluginEnvironmentDeleteTemplateResult, ]; + /** Open one live login pseudo-terminal keyed by a host-owned route identifier. */ + setupTokenPtyOpen: [ + params: PluginSetupTokenPtyOpenParams, + result: PluginSetupTokenPtyOpenResult, + ]; + /** Write delayed input to a live login pseudo-terminal, keyed by the worker session identifier. */ + setupTokenPtyInput: [params: PluginSetupTokenPtyInputParams, result: void]; + /** Stop a live login pseudo-terminal child, keyed by the worker session identifier. */ + setupTokenPtyStop: [params: PluginSetupTokenPtyStopParams, result: void]; + /** Close a live login pseudo-terminal by the host route identifier and return a bound acknowledgement. */ + setupTokenPtyClose: [ + params: PluginSetupTokenPtyCloseParams, + result: PluginSetupTokenPtyCloseResult, + ]; } /** Union of all host→worker method names. */ @@ -1105,6 +1241,10 @@ export const HOST_TO_WORKER_OPTIONAL_METHODS: readonly HostToWorkerMethodName[] "environmentCaptureTemplate", "environmentCancelInteractiveSetup", "environmentDeleteTemplate", + "setupTokenPtyOpen", + "setupTokenPtyInput", + "setupTokenPtyStop", + "setupTokenPtyClose", ] as const; // --------------------------------------------------------------------------- diff --git a/packages/plugins/sdk/src/testing.ts b/packages/plugins/sdk/src/testing.ts index af12e9a8c8..cf0c05b919 100644 --- a/packages/plugins/sdk/src/testing.ts +++ b/packages/plugins/sdk/src/testing.ts @@ -2478,6 +2478,14 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { // No-op in test harness — the host runner log sink is not wired here. }, }, + setupTokenPty: { + output(_workerSessionId: string, _chunk: string) { + // No-op in test harness — the host login route is not wired here. + }, + exit(_workerSessionId: string, _exitCode: number | null) { + // No-op in test harness — the host login route is not wired here. + }, + }, tools: { register(name, _decl, fn) { requireCapability(manifest, capabilitySet, "agent.tools.register"); diff --git a/packages/plugins/sdk/src/types.ts b/packages/plugins/sdk/src/types.ts index adfad498d9..455e70592f 100644 --- a/packages/plugins/sdk/src/types.ts +++ b/packages/plugins/sdk/src/types.ts @@ -2008,6 +2008,35 @@ export interface PluginExecutionClient { log(stream: "stdout" | "stderr", chunk: string): void; } +/** + * `ctx.setupTokenPty` — stream one live login pseudo-terminal's output and exit + * from a sandbox provider worker to the host. + * + * The worker opener registers the output listener on the session and forwards + * each raw chunk through `output(workerSessionId, chunk)`. It forwards the child + * exit through `exit(workerSessionId, exitCode)`. Each call carries the worker + * session identifier the open reply returned, so the host binds the output to + * the open route by that identifier while the route is open. The host drops a + * chunk or an exit that carries an unknown or a mismatched identifier, and it + * never logs the raw bytes. The default is a no-op that never throws. + */ +export interface PluginSetupTokenPtyClient { + /** + * Deliver one raw output chunk of a live login pseudo-terminal. + * + * @param workerSessionId - The worker session identifier the open reply returned. + * @param chunk - The raw terminal output text. + */ + output(workerSessionId: string, chunk: string): void; + /** + * Deliver the child exit of a live login pseudo-terminal. + * + * @param workerSessionId - The worker session identifier the open reply returned. + * @param exitCode - The child exit code, or null when the child ended with no code. + */ + exit(workerSessionId: string, exitCode: number | null): void; +} + // --------------------------------------------------------------------------- // Full plugin context // --------------------------------------------------------------------------- @@ -2123,6 +2152,11 @@ export interface PluginContext { * stream. */ execution: PluginExecutionClient; + /** Stream one live login pseudo-terminal's output and exit to the host. + * The default is a no-op for a provider that opens no login + * pseudo-terminal. */ + setupTokenPty: PluginSetupTokenPtyClient; + /** Register agent tool handlers. Requires `agent.tools.register`. */ tools: PluginToolsClient; diff --git a/packages/plugins/sdk/src/worker-rpc-host.ts b/packages/plugins/sdk/src/worker-rpc-host.ts index ccad211040..dc1b095d2c 100644 --- a/packages/plugins/sdk/src/worker-rpc-host.ts +++ b/packages/plugins/sdk/src/worker-rpc-host.ts @@ -100,11 +100,17 @@ import type { PluginEnvironmentCaptureTemplateParams, PluginEnvironmentCancelInteractiveSetupParams, PluginEnvironmentDeleteTemplateParams, + PluginSetupTokenPtyOpenParams, + PluginSetupTokenPtyInputParams, + PluginSetupTokenPtyStopParams, + PluginSetupTokenPtyCloseParams, PluginInvocationContext, WorkerToHostMethodName, WorkerToHostMethods, } from "./protocol.js"; import { + SETUP_TOKEN_PTY_OUTPUT_NOTIFICATION, + SETUP_TOKEN_PTY_EXIT_NOTIFICATION, JSONRPC_VERSION, JSONRPC_ERROR_CODES, PLUGIN_RPC_ERROR_CODES, @@ -1366,6 +1372,30 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost }, }, + setupTokenPty: { + output(workerSessionId: string, chunk: string): void { + // Forward one raw output chunk of a live login pseudo-terminal. The + // notification carries the worker session identifier, so the host binds + // the chunk to the open route by that identifier while the route is + // open. The host drops an unknown or a mismatched identifier and never + // logs the raw bytes. This notification carries no invocation id, + // because it fires after the open reply returns. + if (typeof workerSessionId !== "string" || workerSessionId.length === 0) return; + if (typeof chunk !== "string" || chunk.length === 0) return; + notifyHost(SETUP_TOKEN_PTY_OUTPUT_NOTIFICATION, { workerSessionId, chunk }); + }, + exit(workerSessionId: string, exitCode: number | null): void { + // Forward the child exit of a live login pseudo-terminal. The host + // resolves the open route's wait promise by the worker session + // identifier while the route is open. + if (typeof workerSessionId !== "string" || workerSessionId.length === 0) return; + notifyHost(SETUP_TOKEN_PTY_EXIT_NOTIFICATION, { + workerSessionId, + exitCode: typeof exitCode === "number" ? exitCode : null, + }); + }, + }, + tools: { register( name: string, @@ -1582,6 +1612,18 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost case "environmentDeleteTemplate": return handleEnvironmentDeleteTemplate(params as PluginEnvironmentDeleteTemplateParams); + case "setupTokenPtyOpen": + return handleSetupTokenPtyOpen(params as PluginSetupTokenPtyOpenParams); + + case "setupTokenPtyInput": + return handleSetupTokenPtyInput(params as PluginSetupTokenPtyInputParams); + + case "setupTokenPtyStop": + return handleSetupTokenPtyStop(params as PluginSetupTokenPtyStopParams); + + case "setupTokenPtyClose": + return handleSetupTokenPtyClose(params as PluginSetupTokenPtyCloseParams); + default: throw Object.assign( new Error(`Unknown method: ${method}`), @@ -1633,6 +1675,10 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost if (plugin.definition.onEnvironmentCaptureTemplate) supportedMethods.push("environmentCaptureTemplate"); if (plugin.definition.onEnvironmentCancelInteractiveSetup) supportedMethods.push("environmentCancelInteractiveSetup"); if (plugin.definition.onEnvironmentDeleteTemplate) supportedMethods.push("environmentDeleteTemplate"); + if (plugin.definition.onSetupTokenPtyOpen) supportedMethods.push("setupTokenPtyOpen"); + if (plugin.definition.onSetupTokenPtyInput) supportedMethods.push("setupTokenPtyInput"); + if (plugin.definition.onSetupTokenPtyStop) supportedMethods.push("setupTokenPtyStop"); + if (plugin.definition.onSetupTokenPtyClose) supportedMethods.push("setupTokenPtyClose"); return { ok: true, supportedMethods }; } @@ -1981,6 +2027,34 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost return plugin.definition.onEnvironmentDeleteTemplate(params); } + async function handleSetupTokenPtyOpen(params: PluginSetupTokenPtyOpenParams) { + if (!plugin.definition.onSetupTokenPtyOpen) { + throw methodNotImplemented("setupTokenPtyOpen"); + } + return plugin.definition.onSetupTokenPtyOpen(params); + } + + async function handleSetupTokenPtyInput(params: PluginSetupTokenPtyInputParams) { + if (!plugin.definition.onSetupTokenPtyInput) { + throw methodNotImplemented("setupTokenPtyInput"); + } + return plugin.definition.onSetupTokenPtyInput(params); + } + + async function handleSetupTokenPtyStop(params: PluginSetupTokenPtyStopParams) { + if (!plugin.definition.onSetupTokenPtyStop) { + throw methodNotImplemented("setupTokenPtyStop"); + } + return plugin.definition.onSetupTokenPtyStop(params); + } + + async function handleSetupTokenPtyClose(params: PluginSetupTokenPtyCloseParams) { + if (!plugin.definition.onSetupTokenPtyClose) { + throw methodNotImplemented("setupTokenPtyClose"); + } + return plugin.definition.onSetupTokenPtyClose(params); + } + // ----------------------------------------------------------------------- // Event filter helper // ----------------------------------------------------------------------- diff --git a/packages/plugins/sdk/tests/worker-rpc-host.test.ts b/packages/plugins/sdk/tests/worker-rpc-host.test.ts index c178a2fe2e..9282496281 100644 --- a/packages/plugins/sdk/tests/worker-rpc-host.test.ts +++ b/packages/plugins/sdk/tests/worker-rpc-host.test.ts @@ -14,9 +14,11 @@ import { createSuccessResponse, isJsonRpcRequest, isJsonRpcResponse, + isJsonRpcNotification, parseMessage, PLUGIN_RPC_ERROR_CODES, serializeMessage, + type JsonRpcNotification, type JsonRpcResponse, type PluginInvocationContext, } from "../src/protocol.js"; @@ -755,3 +757,151 @@ describe("worker execute.log emitter", () => { ]); }); }); + +describe("worker setup-token pseudo-terminal dispatch", () => { + it("dispatches open, input, stop, and close, and streams output and exit as notifications", async () => { + const hostToWorker = new PassThrough(); + const workerToHost = new PassThrough(); + const hostReadline = createInterface({ input: workerToHost }); + const pending = new Map void>(); + const notifications: JsonRpcNotification[] = []; + let nextRequestId = 1; + + // The fake session the opener returns. The test drives its output and exit. + let emitOutput: ((chunk: string) => void) | null = null; + let resolveWait: ((value: { exitCode: number | null }) => void) | null = null; + const inputs: string[] = []; + let killed = 0; + let closed = 0; + + // The worker emits output and exit through `ctx.setupTokenPty`, bound to the + // worker session id. The test drives them through the captured emitters. + const controllablePlugin = definePlugin({ + async setup(ctx) { + emitOutput = (chunk: string) => ctx.setupTokenPty.output("ws-1", chunk); + resolveWait = (value) => ctx.setupTokenPty.exit("ws-1", value.exitCode); + }, + async onSetupTokenPtyOpen(params) { + // The open carries the host route id and the fixed command. The worker + // returns a worker session id for the output binding only. + expect(params.hostRouteId).toBe("route-1"); + expect(params.command).toBe("claude setup-token"); + expect(params.providerLeaseId).toBe("lease-1"); + return { workerSessionId: "ws-1" }; + }, + async onSetupTokenPtyInput(params) { + inputs.push(params.data); + }, + async onSetupTokenPtyStop() { + killed += 1; + }, + async onSetupTokenPtyClose(params) { + // The close keys on the host route id and returns a bound acknowledgement. + closed += 1; + return { hostRouteId: params.hostRouteId }; + }, + }); + + const worker = startWorkerRpcHost({ + plugin: controllablePlugin, + stdin: hostToWorker, + stdout: workerToHost, + }); + + function callWorker(method: string, params: unknown) { + const id = `host-${nextRequestId++}`; + const result = new Promise((resolve, reject) => { + pending.set(id, (response) => { + if ("error" in response && response.error) { + reject(new Error(response.error.message)); + return; + } + resolve((response as { result?: unknown }).result); + }); + }); + hostToWorker.write(serializeMessage(createRequest(method, params, id))); + return result; + } + + hostReadline.on("line", (line) => { + const message = parseMessage(line); + if (isJsonRpcResponse(message)) { + pending.get(String(message.id))?.(message); + pending.delete(String(message.id)); + return; + } + if (isJsonRpcNotification(message)) { + notifications.push(message as JsonRpcNotification); + } + }); + + try { + await expect( + callWorker("initialize", { + manifest: { + id: "paperclip.setup-token-pty", + apiVersion: 1, + version: "1.0.0", + displayName: "Setup Token PTY Test", + description: "Test plugin", + author: "Paperclip", + categories: ["automation"], + capabilities: [], + entrypoints: {}, + }, + config: {}, + databaseNamespace: null, + }), + ).resolves.toMatchObject({ + ok: true, + supportedMethods: expect.arrayContaining([ + "setupTokenPtyOpen", + "setupTokenPtyInput", + "setupTokenPtyStop", + "setupTokenPtyClose", + ]), + }); + + await expect( + callWorker("setupTokenPtyOpen", { + hostRouteId: "route-1", + driverKey: "daytona", + companyId: "company-1", + environmentId: "env-1", + providerLeaseId: "lease-1", + command: "claude setup-token", + }), + ).resolves.toEqual({ workerSessionId: "ws-1" }); + + // The worker streams output as a notification bound to the worker session id. + emitOutput?.("prompt output"); + await callWorker("setupTokenPtyInput", { workerSessionId: "ws-1", data: "browser-code" }); + await callWorker("setupTokenPtyStop", { workerSessionId: "ws-1" }); + resolveWait?.({ exitCode: 0 }); + await expect( + callWorker("setupTokenPtyClose", { hostRouteId: "route-1" }), + ).resolves.toEqual({ hostRouteId: "route-1" }); + + await new Promise((resolve) => setImmediate(resolve)); + + expect(inputs).toEqual(["browser-code"]); + expect(killed).toBe(1); + expect(closed).toBe(1); + const outputNotes = notifications.filter( + (note) => note.method === "setupTokenPty.output", + ); + expect(outputNotes.map((note) => note.params)).toEqual([ + { workerSessionId: "ws-1", chunk: "prompt output" }, + ]); + const exitNotes = notifications.filter( + (note) => note.method === "setupTokenPty.exit", + ); + expect(exitNotes.map((note) => note.params)).toEqual([ + { workerSessionId: "ws-1", exitCode: 0 }, + ]); + } finally { + worker.stop(); + hostReadline.close(); + } + }); +}); diff --git a/packages/shared/src/environment-support.ts b/packages/shared/src/environment-support.ts index 2f0c808470..bc8b56df76 100644 --- a/packages/shared/src/environment-support.ts +++ b/packages/shared/src/environment-support.ts @@ -49,6 +49,7 @@ export interface EnvironmentProviderCapability { templateRefKind?: string; templateConfigBinding?: PluginEnvironmentTemplateConfigBinding; supportsTemplateDelete: boolean; + supportsSetupTokenLogin: boolean; displayName?: string; description?: string; source?: "builtin" | "plugin"; @@ -156,6 +157,7 @@ export function getEnvironmentCapabilities( interactiveSetupConnectionTypes: [], supportsTemplateCapture: false, supportsTemplateDelete: false, + supportsSetupTokenLogin: false, displayName: "Fake", source: "builtin", }, @@ -175,6 +177,7 @@ export function getEnvironmentCapabilities( templateRefKind: capability.templateRefKind, templateConfigBinding: capability.templateConfigBinding, supportsTemplateDelete: capability.supportsTemplateDelete ?? false, + supportsSetupTokenLogin: capability.supportsSetupTokenLogin ?? false, displayName: capability.displayName, description: capability.description, source: capability.source ?? "plugin", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 51f8032ebf..9c605750cb 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -816,6 +816,14 @@ export type { AdapterAuthSessionPrompt, AdapterAuthSessionOwnerResponse, StartAdapterAuthSessionRequest, + AdapterAuthPanelMode, + ClaudeSetupTokenSessionPrompt, + ClaudeSetupTokenSessionResponse, + ClaudeSetupTokenSessionOwnerResponse, + SubmitBrowserCodeRequest, + ClaudeSetupTokenCompletionResponse, + SetupTokenTransportAdvisory, + SetupTokenTransportAdvisoryCode, AssetImage, Project, ProjectBudgetSummary, @@ -1436,6 +1444,8 @@ export { export { ADAPTER_AUTH_SESSION_STATUSES, ADAPTER_AUTH_SESSION_INTERNAL_STATUSES, + ADAPTER_AUTH_PANEL_MODES, + SETUP_TOKEN_TRANSPORT_ADVISORY_CODE, } from "./types/index.js"; export { ADAPTER_AUTH_SESSION_ACTIVE_STATUSES, @@ -1451,6 +1461,27 @@ export { adapterAuthSessionOwnerResponseSchema, startAdapterAuthSessionRequestSchema, } from "./validators/adapter-auth-session.js"; +export { + startClaudeSetupTokenSessionRequestSchema, + adapterAuthPanelModeSchema, + claudeSetupTokenSessionResponseSchema, + claudeSetupTokenSessionPromptSchema, + claudeSetupTokenSessionOwnerResponseSchema, + setupTokenTransportAdvisorySchema, + submitBrowserCodeRequestSchema, + browserCodeSchema, + isValidBrowserCode, + BROWSER_CODE_MAX_LENGTH, + BROWSER_CODE_DISALLOWED_CHAR, + storedSessionIdSchema, + claudeSetupTokenCompletionResponseSchema, + claudeSetupTokenOverwriteSchema, + claudeOAuthTokenStatusResponseSchema, + type StartClaudeSetupTokenSessionRequest, + type ClaudeSetupTokenOverwrite, + type ClaudeOAuthTokenStatusResponse, + type BrowserCode, +} from "./validators/claude-setup-token-session.js"; export { ISSUE_REFERENCE_IDENTIFIER_RE, buildIssueReferenceHref, diff --git a/packages/shared/src/types/agent.ts b/packages/shared/src/types/agent.ts index bea8794ea0..9f7453843d 100644 --- a/packages/shared/src/types/agent.ts +++ b/packages/shared/src/types/agent.ts @@ -204,6 +204,95 @@ export interface StartAdapterAuthSessionRequest { ttlSeconds?: number; } +// The login-panel mode. It tells the client which login panel to render. +// +// - `displayed_code`: the server shows a one-time code. The user reads the code +// into the provider prompt. The Codex login prompt above uses this mode. +// - `submitted_browser_code`: the provider shows a code in the browser. The user +// submits that code back to the server. The Claude login uses this mode. +export const ADAPTER_AUTH_PANEL_MODES = [ + "displayed_code", + "submitted_browser_code", +] as const; +export type AdapterAuthPanelMode = (typeof ADAPTER_AUTH_PANEL_MODES)[number]; + +// The company-and-environment Claude login session. The scope binds one login to +// one company, one owner user, one adapter, and one environment. The scope +// carries no agent id, so a hire flow with no agent still starts one session. +// +// The Codex prompt above carries a server-displayed code. The Claude login is +// different: the provider shows a browser code, and the user submits that code. +// So this contract carries a submitted browser code, not a displayed code. + +// The transport advisory for a setup-token confidential response. The product +// owner set a non-negotiable requirement: do not force TLS. Many users run +// Paperclip over plain HTTP on a home server or a Tailscale tailnet. So the +// setup-token routes do not block a non-confidential transport. They attach this +// advisory to the confidential response instead. The client shows a visible, +// non-blocking disclaimer and the login still proceeds. A confidential transport +// (direct TLS, a local-trusted loopback, or an allowlisted TLS proxy) carries no +// advisory. +export const SETUP_TOKEN_TRANSPORT_ADVISORY_CODE = "insecure_transport" as const; +export type SetupTokenTransportAdvisoryCode = typeof SETUP_TOKEN_TRANSPORT_ADVISORY_CODE; + +// The advisory signal on a setup-token confidential response. A `null` or an +// absent value means the transport is confidential and needs no disclaimer. +export interface SetupTokenTransportAdvisory { + code: SetupTokenTransportAdvisoryCode; +} + +// The one-time Claude login prompt. The server returns it only through an owner +// read. It carries the authorization URL the user opens. The provider shows the +// browser code in the browser; the user submits that code back to the server. +// This prompt carries no server-displayed code. +export interface ClaudeSetupTokenSessionPrompt { + authorizationUrl: string; + // The transport advisory. It is present and non-null when the login rides a + // non-confidential transport. The client shows a disclaimer, not a block. + transportAdvisory?: SetupTokenTransportAdvisory | null; +} + +// The public Claude login-session response. It reuses the adapter login-session +// response fields; every field has the same meaning. It carries no secret. It +// never carries the browser code, a token, an account identifier, or the +// provider lease identifier. The `status` is always a public status. +export interface ClaudeSetupTokenSessionResponse { + sessionId: string; + environmentId: string; + status: AdapterAuthSessionStatus; + expiresAt: string | null; + failure: AdapterAuthSessionFailure | null; + // The transport advisory. A guarded route (the browser-code submit) sets it + // when the login rides a non-confidential transport. The status and the start + // responses omit it, because they carry no confidential value. + transportAdvisory?: SetupTokenTransportAdvisory | null; +} + +// The owner read of a Claude login session. It adds the panel mode and the +// one-time prompt to the public response. Only the owner principal that started +// the session reads this shape. +export interface ClaudeSetupTokenSessionOwnerResponse + extends ClaudeSetupTokenSessionResponse { + panelMode: AdapterAuthPanelMode; + prompt: ClaudeSetupTokenSessionPrompt | null; +} + +// The request that submits the browser code for a Claude login session. The user +// copies the code from the browser and submits it here. The validator rejects a +// control byte and an oversized code, so a malformed code never reaches the +// live login process. +export interface SubmitBrowserCodeRequest { + browserCode: string; +} + +// The completion response for a Claude login session. It carries the non-secret +// `storedSessionId` claim and no token. The `storedSessionId` is the durable +// session id; the agent-create transaction consumes it as the one-time +// stored-session claim. +export interface ClaudeSetupTokenCompletionResponse { + storedSessionId: string; +} + export type AdapterEnvironmentCheckLevel = "info" | "warn" | "error"; export type AdapterEnvironmentTestStatus = "pass" | "warn" | "fail"; diff --git a/packages/shared/src/types/environment.ts b/packages/shared/src/types/environment.ts index 2a2867ffc1..8384a63b1d 100644 --- a/packages/shared/src/types/environment.ts +++ b/packages/shared/src/types/environment.ts @@ -83,7 +83,11 @@ export interface Environment { updatedAt: Date; } -export type EnvironmentDeleteBlockedReason = "managed_local" | "instance_default"; +export type EnvironmentDeleteBlockedReason = + | "managed_local" + | "instance_default" + | "reusable_sandbox_lease" + | "pending_sandbox_cleanup"; export interface EnvironmentDeleteBlastRadius { environmentId: string; @@ -103,12 +107,28 @@ export interface EnvironmentDeleteBlastRadius { activeCustomImageSetupSessionCount: number; hasActiveRuntimeUse: boolean; }; + /** + * Count of leases in the terminal `pending_cleanup` state. Each such lease + * holds the only durable provider reference for a sandbox that a teardown + * retry must destroy. A delete or a provider change must not remove this + * reference, so a count above zero blocks the delete. + */ + pendingCleanupLeaseCount: number; + /** + * Count of reusable sandbox leases whose provider resource is still live. + * Deleting the environment would remove the context used by their normal + * release and destroy paths, so these leases block deletion. + */ + reusableSandboxLeaseCount: number; } export interface EnvironmentLease { id: string; companyId: string; - environmentId: string; + // Null only for an orphan `pending_cleanup` lease whose environment a delete + // removed. The cleanup sweep tears the orphan down from the row's immutable + // provider metadata, so it needs no environment row. + environmentId: string | null; executionWorkspaceId: string | null; issueId: string | null; heartbeatRunId: string | null; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 82f1da7400..1bc7a38fc3 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -285,10 +285,20 @@ export type { AdapterAuthSessionPrompt, AdapterAuthSessionOwnerResponse, StartAdapterAuthSessionRequest, + AdapterAuthPanelMode, + ClaudeSetupTokenSessionPrompt, + ClaudeSetupTokenSessionResponse, + ClaudeSetupTokenSessionOwnerResponse, + SubmitBrowserCodeRequest, + ClaudeSetupTokenCompletionResponse, + SetupTokenTransportAdvisory, + SetupTokenTransportAdvisoryCode, } from "./agent.js"; export { ADAPTER_AUTH_SESSION_STATUSES, ADAPTER_AUTH_SESSION_INTERNAL_STATUSES, + ADAPTER_AUTH_PANEL_MODES, + SETUP_TOKEN_TRANSPORT_ADVISORY_CODE, } from "./agent.js"; export type { AgentEligibilityAgent, diff --git a/packages/shared/src/types/plugin.ts b/packages/shared/src/types/plugin.ts index 2d14f567d6..3268a3a847 100644 --- a/packages/shared/src/types/plugin.ts +++ b/packages/shared/src/types/plugin.ts @@ -215,6 +215,13 @@ export interface PluginEnvironmentDriverDeclaration { templateIdentityPaths?: string[]; /** Provider supports best-effort deletion/cleanup of captured templates. */ supportsTemplateDelete?: boolean; + /** + * Provider can host the Claude setup-token login on a real pseudo-terminal. + * Only a provider with this flag exposes the setup-token pseudo-terminal + * methods. The setup-token login server and the login UI both gate on this + * flag, so a provider without it never starts a login. + */ + supportsSetupTokenLogin?: boolean; /** JSON Schema describing the driver's provider-specific configuration. */ configSchema: JsonSchema; } diff --git a/packages/shared/src/validators/agent.ts b/packages/shared/src/validators/agent.ts index a0d38cd921..308cd057c1 100644 --- a/packages/shared/src/validators/agent.ts +++ b/packages/shared/src/validators/agent.ts @@ -84,6 +84,15 @@ export const createAgentSchema = z.object({ budgetMonthlyCents: z.number().int().nonnegative().optional().default(0), permissions: agentPermissionsSchema.optional(), metadata: z.record(z.string(), z.unknown()).optional().nullable(), + // The optional stored-session claim from a completed Claude login session. It + // is the non-secret `storedSessionId`; it carries no token. The agent-create + // transaction consumes it as the one-time stored-session claim. + storedSessionId: z.string().min(1).max(256).optional(), + // The optional apply-existing flag. When true, the caller binds the fixed + // Claude OAuth token reference to the owner stored value with no new login + // round trip. The server permits the no-claim bind only for a user actor and + // only when that owner already has a stored value. It carries no token. + applyStoredClaudeLogin: z.boolean().optional(), }); export type CreateAgent = z.infer; diff --git a/packages/shared/src/validators/claude-setup-token-session.test.ts b/packages/shared/src/validators/claude-setup-token-session.test.ts new file mode 100644 index 0000000000..2e672819bf --- /dev/null +++ b/packages/shared/src/validators/claude-setup-token-session.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, it } from "vitest"; +import { + BROWSER_CODE_MAX_LENGTH, + browserCodeSchema, + claudeSetupTokenCompletionResponseSchema, + claudeSetupTokenSessionOwnerResponseSchema, + isValidBrowserCode, + startClaudeSetupTokenSessionRequestSchema, + submitBrowserCodeRequestSchema, +} from "./claude-setup-token-session.js"; +import { createAgentSchema, createAgentHireSchema } from "./agent.js"; + +const ENVIRONMENT_ID = "11111111-1111-4111-8111-111111111111"; + +describe("claude setup-token session start request", () => { + it("accepts a company-and-environment request with no agent id", () => { + const parsed = startClaudeSetupTokenSessionRequestSchema.parse({ + environmentId: ENVIRONMENT_ID, + adapterType: "claude_local", + }); + expect(parsed.environmentId).toBe(ENVIRONMENT_ID); + expect(parsed.adapterType).toBe("claude_local"); + }); + + it("rejects a legacy ttlSeconds: the runtime does not support it", () => { + const result = startClaudeSetupTokenSessionRequestSchema.safeParse({ + environmentId: ENVIRONMENT_ID, + adapterType: "claude_local", + ttlSeconds: 300, + }); + expect(result.success).toBe(false); + }); + + it("rejects an agent id: the scope carries no agent id", () => { + const result = startClaudeSetupTokenSessionRequestSchema.safeParse({ + environmentId: ENVIRONMENT_ID, + adapterType: "claude_local", + agentId: "22222222-2222-4222-8222-222222222222", + }); + expect(result.success).toBe(false); + }); +}); + +describe("browser-code grammar", () => { + it("accepts a conservative printable code", () => { + const code = "ac_012-AZ_az.9#state"; + expect(browserCodeSchema.parse(code)).toBe(code); + expect(isValidBrowserCode(code)).toBe(true); + }); + + it("rejects a carriage return, a line feed, a NUL, and a control byte", () => { + for (const bad of ["ab\rcd", "ab\ncd", "ab\u0000cd", "ab\u0001cd", "ab\u007fcd", "ab cd", "ab\tcd"]) { + expect(browserCodeSchema.safeParse(bad).success).toBe(false); + expect(isValidBrowserCode(bad)).toBe(false); + } + }); + + it("rejects a trailing newline that an anchored regex would let pass", () => { + // The runtime `.refine()` is the authoritative guard. JavaScript `$` + // matches before a final line feed, so an anchored printable-ASCII regex + // accepts a trailing newline. The `.refine()` closes that trap. + expect(browserCodeSchema.safeParse("abc\n").success).toBe(false); + expect(isValidBrowserCode("abc\n")).toBe(false); + }); + + it("rejects a single control byte and accepts a printable code", () => { + expect(browserCodeSchema.safeParse("abc").success).toBe(true); + expect(browserCodeSchema.safeParse("valid-code_123").success).toBe(true); + }); + + it("rejects an empty code", () => { + expect(browserCodeSchema.safeParse("").success).toBe(false); + expect(isValidBrowserCode("")).toBe(false); + }); + + it("rejects an oversized code", () => { + const oversized = "a".repeat(BROWSER_CODE_MAX_LENGTH + 1); + expect(browserCodeSchema.safeParse(oversized).success).toBe(false); + expect(isValidBrowserCode(oversized)).toBe(false); + const atLimit = "a".repeat(BROWSER_CODE_MAX_LENGTH); + expect(browserCodeSchema.parse(atLimit)).toBe(atLimit); + }); + + it("wraps the grammar in the submit-browser-code request and rejects an extra field", () => { + expect(submitBrowserCodeRequestSchema.parse({ browserCode: "abc123" }).browserCode).toBe( + "abc123", + ); + expect(submitBrowserCodeRequestSchema.safeParse({ browserCode: "a\nb" }).success).toBe(false); + expect( + submitBrowserCodeRequestSchema.safeParse({ browserCode: "abc", extra: 1 }).success, + ).toBe(false); + }); +}); + +describe("claude setup-token owner response and completion response", () => { + it("accepts an owner response with the submitted-browser-code panel mode and a prompt", () => { + const parsed = claudeSetupTokenSessionOwnerResponseSchema.parse({ + sessionId: "opaque-base64url-session-id", + environmentId: ENVIRONMENT_ID, + status: "waiting_for_user", + expiresAt: null, + failure: null, + panelMode: "submitted_browser_code", + prompt: { authorizationUrl: "https://example.test/authorize" }, + }); + expect(parsed.panelMode).toBe("submitted_browser_code"); + expect(parsed.prompt?.authorizationUrl).toBe("https://example.test/authorize"); + }); + + it("rejects a token on the completion response: the claim carries no token", () => { + expect( + claudeSetupTokenCompletionResponseSchema.parse({ storedSessionId: "session-claim" }) + .storedSessionId, + ).toBe("session-claim"); + const withToken = claudeSetupTokenCompletionResponseSchema.safeParse({ + storedSessionId: "session-claim", + token: "secret", + }); + expect(withToken.success).toBe(false); + }); +}); + +describe("agent create and hire carry an optional stored-session claim", () => { + const base = { name: "Claude Agent", adapterType: "claude_local" as const }; + + it("accepts a create request without a stored-session claim", () => { + expect(createAgentSchema.parse({ ...base }).storedSessionId).toBeUndefined(); + }); + + it("accepts a create request with a stored-session claim", () => { + expect( + createAgentSchema.parse({ ...base, storedSessionId: "session-claim" }).storedSessionId, + ).toBe("session-claim"); + }); + + it("accepts a hire request with a stored-session claim", () => { + expect( + createAgentHireSchema.parse({ ...base, storedSessionId: "session-claim" }).storedSessionId, + ).toBe("session-claim"); + }); +}); diff --git a/packages/shared/src/validators/claude-setup-token-session.ts b/packages/shared/src/validators/claude-setup-token-session.ts new file mode 100644 index 0000000000..f2dbe68357 --- /dev/null +++ b/packages/shared/src/validators/claude-setup-token-session.ts @@ -0,0 +1,170 @@ +import { z } from "zod"; +import { AGENT_ADAPTER_TYPES } from "../constants.js"; +import { ADAPTER_AUTH_PANEL_MODES, SETUP_TOKEN_TRANSPORT_ADVISORY_CODE } from "../types/agent.js"; +import { + adapterAuthSessionFailureSchema, + adapterAuthSessionStatusSchema, +} from "./adapter-auth-session.js"; + +const isoDateTime = z.union([z.date(), z.string().datetime()]); + +// The transport advisory schema. A guarded confidential response carries it when +// the login rides a non-confidential transport. The client shows a disclaimer. +export const setupTokenTransportAdvisorySchema = z.object({ + code: z.literal(SETUP_TOKEN_TRANSPORT_ADVISORY_CODE), +}).strict(); +export type SetupTokenTransportAdvisory = z.infer; + +// The confirmed-overwrite capture for a replacement login. The client reads the +// stored value metadata from the status route and passes it back when it starts +// a replacement login. The server maps it to the owner-bound compare-and-set: it +// rotates the value only when the captured version still matches. The server +// still derives the owner and the definition; this body carries no owner. A +// missing capture starts a first-write login instead. +export const claudeSetupTokenOverwriteSchema = z.object({ + expectedSecretId: z.string().uuid(), + expectedLatestVersion: z.number().int().min(1), +}).strict(); +export type ClaudeSetupTokenOverwrite = + z.infer; + +// The start request for a company-and-environment Claude login session. The +// company and the owner user come from the authenticated caller, not from this +// body. The body names the adapter and the environment of the login. It reuses +// the adapter login-session request fields; every field has the same meaning. +// +// The optional `overwrite` capture turns the login into a confirmed replacement. +// The client sends it only after a stored token fails the agent test. The server +// rotates the stored value under the captured version, so a concurrent change +// fails closed with a stale conflict. +// +// `.strict()` rejects an extra field, so an agent id never validates. The scope +// carries no agent id: a hire flow with no agent still starts one session. The +// runtime does not support a caller-supplied session length, so the schema +// exposes no `ttlSeconds` field; a legacy `ttlSeconds` fails the strict parse. +export const startClaudeSetupTokenSessionRequestSchema = z.object({ + environmentId: z.string().uuid(), + adapterType: z.enum(AGENT_ADAPTER_TYPES), + overwrite: claudeSetupTokenOverwriteSchema.optional(), +}).strict(); +export type StartClaudeSetupTokenSessionRequest = + z.infer; + +// The panel-mode schema. It accepts only the two known panel modes. +export const adapterAuthPanelModeSchema = z.enum(ADAPTER_AUTH_PANEL_MODES); +export type AdapterAuthPanelMode = z.infer; + +// The session id is an opaque, cryptographically random string, not a UUID. So +// the schema accepts a bounded opaque string, not a UUID. +const sessionIdSchema = z.string().min(1).max(256); + +// The public Claude login-session response schema. `.strict()` rejects an extra +// field, so a prompt, a token, an account identifier, or a provider lease +// identifier never validates. +export const claudeSetupTokenSessionResponseSchema = z.object({ + sessionId: sessionIdSchema, + environmentId: z.string().uuid(), + status: adapterAuthSessionStatusSchema, + expiresAt: isoDateTime.nullable(), + failure: adapterAuthSessionFailureSchema.nullable(), + transportAdvisory: setupTokenTransportAdvisorySchema.nullable().optional(), +}).strict(); +export type ClaudeSetupTokenSessionResponse = + z.infer; + +// The one-time Claude login prompt schema. It carries the authorization URL the +// user opens. It carries no server-displayed code. +export const claudeSetupTokenSessionPromptSchema = z.object({ + authorizationUrl: z.string().min(1), + transportAdvisory: setupTokenTransportAdvisorySchema.nullable().optional(), +}).strict(); +export type ClaudeSetupTokenSessionPrompt = + z.infer; + +// The owner read schema. It adds the panel mode and the one-time prompt to the +// public response. +export const claudeSetupTokenSessionOwnerResponseSchema = + claudeSetupTokenSessionResponseSchema.extend({ + panelMode: adapterAuthPanelModeSchema, + prompt: claudeSetupTokenSessionPromptSchema.nullable(), + }).strict(); +export type ClaudeSetupTokenSessionOwnerResponse = + z.infer; + +// The bounded maximum length of a browser code. The provider code is short. The +// server rejects an oversized code before it reaches the live login process. +export const BROWSER_CODE_MAX_LENGTH = 512; + +// The conservative printable grammar for the submitted browser code. It matches +// one character outside the visible ASCII range (0x21-0x7E). The validator +// rejects a code that contains a match, so it rejects a space, a carriage +// return, a line feed, a NUL, and every other control byte. A later +// characterization test narrows this set to the exact provider format. +export const BROWSER_CODE_DISALLOWED_CHAR = /[^\x21-\x7E]/; + +/** + * Returns true when the browser code obeys the grammar. The code has one or more + * characters, no character outside the visible ASCII range, and a length at or + * below the bounded maximum. + */ +export function isValidBrowserCode(code: string): boolean { + return ( + code.length >= 1 && + code.length <= BROWSER_CODE_MAX_LENGTH && + !BROWSER_CODE_DISALLOWED_CHAR.test(code) + ); +} + +// The printable-ASCII allowlist pattern for the published contract. It is an +// anchored allowlist of the visible ASCII range (0x21-0x7E). The converter reads +// this `.regex()` from the inner `ZodString` and emits it as the OpenAPI +// `pattern`. It is NOT the authoritative guard: JavaScript `$` matches before a +// final line feed, so this pattern accepts a trailing newline. The `.refine()` +// below is the authoritative guard that rejects a trailing newline. +export const BROWSER_CODE_PATTERN = /^[\x21-\x7E]+$/u; + +// The browser-code grammar schema. It rejects an empty code, an oversized code, +// and a code with a control byte or any other non-printable character. The +// `.regex()` runs before the `.refine()` so the converter serializes its +// `pattern`; the `.refine()` stays the authoritative guard. +export const browserCodeSchema = z + .string() + .min(1, "A browser code is required.") + .max(BROWSER_CODE_MAX_LENGTH, "The browser code is too long.") + .regex(BROWSER_CODE_PATTERN, "The browser code has an invalid character.") + .refine((code) => !BROWSER_CODE_DISALLOWED_CHAR.test(code), { + message: "The browser code has an invalid character.", + }); +export type BrowserCode = z.infer; + +// The submit-browser-code request schema. `.strict()` rejects an extra field. +export const submitBrowserCodeRequestSchema = z.object({ + browserCode: browserCodeSchema, +}).strict(); +export type SubmitBrowserCodeRequest = + z.infer; + +// The stored-session claim schema. The `storedSessionId` is the opaque durable +// session id. It is a non-secret claim; it carries no token. +export const storedSessionIdSchema = z.string().min(1).max(256); + +// The completion response schema. It carries the non-secret `storedSessionId` +// claim and no token. `.strict()` rejects an extra field, so a token never +// validates. +export const claudeSetupTokenCompletionResponseSchema = z.object({ + storedSessionId: storedSessionIdSchema, +}).strict(); +export type ClaudeSetupTokenCompletionResponse = + z.infer; + +// The stored Claude OAuth token status response schema. It carries only the +// secret id and the latest version of the owner value; it carries no token. +// `.strict()` rejects an extra field, so a token never validates. The status +// route returns a fixed 404 for a missing or a foreign value, so a 200 body +// always describes a present owner value. +export const claudeOAuthTokenStatusResponseSchema = z.object({ + secretId: z.string().uuid(), + latestVersion: z.number().int().min(1), +}).strict(); +export type ClaudeOAuthTokenStatusResponse = + z.infer; diff --git a/packages/shared/src/validators/plugin.ts b/packages/shared/src/validators/plugin.ts index 7c048a0f2c..9145f19b79 100644 --- a/packages/shared/src/validators/plugin.ts +++ b/packages/shared/src/validators/plugin.ts @@ -187,6 +187,7 @@ export const pluginEnvironmentDriverDeclarationSchema = z.object({ templateConfigBinding: pluginEnvironmentTemplateConfigBindingSchema.optional(), templateIdentityPaths: z.array(z.string().min(1).max(200)).max(20).optional(), supportsTemplateDelete: z.boolean().optional(), + supportsSetupTokenLogin: z.boolean().optional(), configSchema: jsonSchemaSchema, }); diff --git a/server/src/__tests__/agent-adapter-validation-routes.test.ts b/server/src/__tests__/agent-adapter-validation-routes.test.ts index ff1a00e9d8..8540d622bc 100644 --- a/server/src/__tests__/agent-adapter-validation-routes.test.ts +++ b/server/src/__tests__/agent-adapter-validation-routes.test.ts @@ -354,6 +354,58 @@ describe("agent routes adapter validation", () => { expect(env.CODEX_HOME).toBeUndefined(); }); + it("forwards a claude_local→process adapter move that drops the OAuth binding to the service unchanged", async () => { + // The agent has the fixed Claude Code OAuth binding on the claude_local + // adapter. A PATCH moves the agent to the process adapter and sends an empty + // env in the same request. The route must forward the new adapter type and + // the dropped binding to the service without a re-injection, so the + // service-enforced binding invariant sees the removal and rejects it. + const agentId = "11111111-1111-4111-8111-111111111111"; + mockAgentService.getById.mockResolvedValue({ + id: agentId, + companyId: "company-1", + name: "Claude", + urlKey: "claude", + role: "engineer", + title: null, + icon: null, + status: "idle", + reportsTo: null, + capabilities: null, + adapterType: "claude_local", + adapterConfig: { env: { CLAUDE_CODE_OAUTH_TOKEN: { type: "user_secret_ref", key: "CLAUDE_CODE_OAUTH_TOKEN" } } }, + runtimeConfig: {}, + budgetMonthlyCents: 0, + spentMonthlyCents: 0, + pauseReason: null, + pausedAt: null, + permissions: { canCreateAgents: false }, + lastHeartbeatAt: null, + metadata: null, + createdAt: new Date(), + updatedAt: new Date(), + }); + const app = await createApp(); + const res = await requestApp(app, (baseUrl) => + request(baseUrl) + .patch(`/api/agents/${agentId}`) + .send({ + adapterType: "process", + adapterConfig: { env: {} }, + }), + ); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + const patch = mockAgentService.update.mock.calls.at(-1)?.[1] as Record; + // The route forwards the requested adapter type, so the service can see the + // adapter move. + expect(patch.adapterType).toBe("process"); + // The route does not re-inject the fixed binding from the prior config, so + // the service invariant sees the removal. + const env = ((patch.adapterConfig as Record).env as Record | undefined) ?? {}; + expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined(); + }); + it("isolates CODEX_HOME when updating a codex_local agent to set its own OPENAI_API_KEY", async () => { const agentId = "11111111-1111-4111-8111-111111111111"; const app = await createApp(); diff --git a/server/src/__tests__/agent-device-login-routes.test.ts b/server/src/__tests__/agent-device-login-routes.test.ts index c3bb8568db..d0b2c3869a 100644 --- a/server/src/__tests__/agent-device-login-routes.test.ts +++ b/server/src/__tests__/agent-device-login-routes.test.ts @@ -61,6 +61,9 @@ const mockSecretService = vi.hoisted(() => ({ const mockEnvironmentService = vi.hoisted(() => ({ getById: vi.fn(), releaseLease: vi.fn(), + // The login guard reads the companies that own the environment. An empty + // list marks an instance-global environment, so the guard stays open. + listBoundCompanyIds: vi.fn(async () => [] as string[]), })); const mockEnvironmentRuntime = vi.hoisted(() => ({ diff --git a/server/src/__tests__/agent-permissions-routes.test.ts b/server/src/__tests__/agent-permissions-routes.test.ts index 0f45292f1e..c864762f8f 100644 --- a/server/src/__tests__/agent-permissions-routes.test.ts +++ b/server/src/__tests__/agent-permissions-routes.test.ts @@ -864,6 +864,7 @@ describe.sequential("agent permission routes", () => { expect.objectContaining({ status: "idle", }), + { claudeLogin: { storedSessionId: null, ownerUserId: "agent-admin-user", applyExistingWithoutClaim: false } }, ); expect(mockAccessService.setPrincipalPermission).toHaveBeenCalledWith( companyId, @@ -996,6 +997,7 @@ describe.sequential("agent permission routes", () => { }, }, }), + { claudeLogin: { storedSessionId: null, ownerUserId: "board-user", applyExistingWithoutClaim: false } }, ); }); @@ -1058,6 +1060,7 @@ describe.sequential("agent permission routes", () => { }, }, }), + { claudeLogin: { storedSessionId: null, ownerUserId: "board-user", applyExistingWithoutClaim: false } }, ); } finally { unregisterServerAdapter("failing_profile_discovery"); @@ -1096,6 +1099,7 @@ describe.sequential("agent permission routes", () => { model: DEFAULT_OPENCODE_LOCAL_MODEL, }), }), + { claudeLogin: { storedSessionId: null, ownerUserId: "board-user", applyExistingWithoutClaim: false } }, ); }); @@ -1133,6 +1137,7 @@ describe.sequential("agent permission routes", () => { model: "anthropic/claude-sonnet-4-5", }), }), + { claudeLogin: { storedSessionId: null, ownerUserId: "board-user", applyExistingWithoutClaim: false } }, ); }); @@ -1174,6 +1179,7 @@ describe.sequential("agent permission routes", () => { }, }, }), + { claudeLogin: { storedSessionId: null, ownerUserId: "board-user", applyExistingWithoutClaim: false } }, ); }); @@ -1397,6 +1403,7 @@ describe.sequential("agent permission routes", () => { expect.objectContaining({ defaultEnvironmentId: environmentId, }), + { claudeLogin: { storedSessionId: null, ownerUserId: "board-user", applyExistingWithoutClaim: false } }, ); }); @@ -1482,6 +1489,7 @@ describe.sequential("agent permission routes", () => { adapterType: adapterCase.adapterType, defaultEnvironmentId: environmentId, }), + { claudeLogin: { storedSessionId: null, ownerUserId: "board-user", applyExistingWithoutClaim: false } }, ); }); } diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index 6dd77801ed..47379332d3 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -945,6 +945,7 @@ describe.sequential("agent skill routes", () => { }), }), }), + { claudeLogin: { storedSessionId: null, ownerUserId: "local-board", applyExistingWithoutClaim: false } }, ); expect(mockTrackAgentCreated).toHaveBeenCalledWith( expect.anything(), @@ -994,6 +995,7 @@ describe.sequential("agent skill routes", () => { expect.objectContaining({ role: "security", }), + { claudeLogin: { storedSessionId: null, ownerUserId: "local-board", applyExistingWithoutClaim: false } }, ); expect(mockTrackAgentCreated).toHaveBeenCalledWith( expect.anything(), @@ -1217,6 +1219,7 @@ describe.sequential("agent skill routes", () => { }), }), }), + { claudeLogin: { storedSessionId: null, ownerUserId: "local-board", applyExistingWithoutClaim: false } }, ); expect(mockApprovalService.create).toHaveBeenCalledWith( "company-1", diff --git a/server/src/__tests__/agents-claude-oauth-binding.test.ts b/server/src/__tests__/agents-claude-oauth-binding.test.ts new file mode 100644 index 0000000000..35bdd741fb --- /dev/null +++ b/server/src/__tests__/agents-claude-oauth-binding.test.ts @@ -0,0 +1,948 @@ +import { randomUUID } from "node:crypto"; +import { mkdirSync, rmSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { eq, sql } from "drizzle-orm"; +import { + agents, + claudeSetupTokenSessions, + companies, + companySecretBindings, + companySecretProviderConfigs, + companySecretVersions, + companySecrets, + createDb, + environments, + userSecretDeclarations, + userSecretDefinitions, +} from "@paperclipai/db"; +import { + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "./helpers/embedded-postgres.js"; +import { agentService } from "../services/agents.ts"; +import { + assertClaudeOAuthBindingInvariant, + CLAUDE_OAUTH_CLAIM_REJECTED, + CLAUDE_OAUTH_CREDENTIAL_CONFLICT, + claudeOAuthClaimRejectedError, + isFixedClaudeOAuthBinding, + secretService, +} from "../services/secrets.js"; +import { + createDbSetupTokenCleanupStore, + type SetupTokenSessionScope, +} from "../services/setup-token-session.js"; +import { createSetupTokenSecretWriter } from "../services/setup-token-transport-binding.js"; + +// The fixed Claude Code OAuth binding. It is a user-secret reference to the +// fixed key. Any other shape is a replacement or a weaker binding. +const FIXED_BINDING = { type: "user_secret_ref", key: "CLAUDE_CODE_OAUTH_TOKEN" } as const; +const withEnv = (env: Record) => ({ env }); + +// --- The pure server-enforced binding invariant (no database) ---------------- + +describe("assertClaudeOAuthBindingInvariant", () => { + it("reports that a create introduces the fixed binding", () => { + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "claude_local", + nextConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING }), + priorConfig: null, + }); + expect(decision).toEqual({ introducesBinding: true, keepsBinding: false }); + }); + + it("reports that a write keeps an existing fixed binding", () => { + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "claude_local", + nextConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING }), + priorConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING }), + }); + expect(decision).toEqual({ introducesBinding: false, keepsBinding: true }); + }); + + it("permits a normal removal of the fixed binding", () => { + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "claude_local", + nextConfig: withEnv({}), + priorConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING }), + }); + expect(decision).toEqual({ introducesBinding: false, keepsBinding: false }); + }); + + it("permits a normal plain-value replacement of the fixed binding", () => { + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "claude_local", + nextConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: { type: "plain", value: "sk-fake" } }), + priorConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING }), + }); + expect(decision).toEqual({ introducesBinding: false, keepsBinding: false }); + }); + + it("permits a re-point to a different user-secret key", () => { + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "claude_local", + nextConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: { type: "user_secret_ref", key: "OTHER" } }), + priorConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING }), + }); + expect(decision).toEqual({ introducesBinding: false, keepsBinding: false }); + }); + + it("permits a re-point to a company-secret reference", () => { + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "claude_local", + nextConfig: withEnv({ + CLAUDE_CODE_OAUTH_TOKEN: { type: "secret_ref", secretId: randomUUID(), version: "latest" }, + }), + priorConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING }), + }); + expect(decision).toEqual({ introducesBinding: false, keepsBinding: false }); + }); + + it("permits a removal of the fixed binding by a move to another adapter type", () => { + // The prior config has the fixed binding on claude_local. The write moves + // the agent to the process adapter and drops the binding in the same write. + // The binding behaves like a normal environment variable, so the write is + // allowed and reports no fixed binding. + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "process", + nextConfig: withEnv({}), + priorConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING }), + }); + expect(decision).toEqual({ introducesBinding: false, keepsBinding: false }); + }); + + it("does nothing for a non-claude_local create with no prior fixed binding", () => { + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "process", + nextConfig: withEnv({}), + priorConfig: withEnv({ SOME_OTHER_KEY: { type: "plain", value: "x" } }), + }); + expect(decision).toEqual({ introducesBinding: false, keepsBinding: false }); + }); + + it("rejects the fixed binding together with a plain ANTHROPIC_API_KEY and names no value", () => { + let thrown: Error | null = null; + try { + assertClaudeOAuthBindingInvariant({ + adapterType: "claude_local", + nextConfig: withEnv({ + CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING, + ANTHROPIC_API_KEY: { type: "plain", value: "sk-ant-secret" }, + }), + priorConfig: null, + }); + } catch (error) { + thrown = error as Error; + } + expect(thrown?.message).toBe(CLAUDE_OAUTH_CREDENTIAL_CONFLICT); + expect(thrown?.message).not.toContain("sk-ant-secret"); + expect(thrown?.message).not.toContain("ANTHROPIC_API_KEY"); + }); + + it("rejects the fixed binding together with an ANTHROPIC_API_KEY secret reference", () => { + expect(() => + assertClaudeOAuthBindingInvariant({ + adapterType: "claude_local", + nextConfig: withEnv({ + CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING, + ANTHROPIC_API_KEY: { type: "secret_ref", secretId: randomUUID(), version: "latest" }, + }), + priorConfig: null, + }), + ).toThrowError(CLAUDE_OAUTH_CREDENTIAL_CONFLICT); + }); + + it("ignores an empty ANTHROPIC_API_KEY plain value", () => { + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "claude_local", + nextConfig: withEnv({ + CLAUDE_CODE_OAUTH_TOKEN: FIXED_BINDING, + ANTHROPIC_API_KEY: { type: "plain", value: " " }, + }), + priorConfig: null, + }); + expect(decision.introducesBinding).toBe(true); + }); + + it("does nothing for a non-claude_local adapter", () => { + const decision = assertClaudeOAuthBindingInvariant({ + adapterType: "codex_local", + nextConfig: withEnv({ CLAUDE_CODE_OAUTH_TOKEN: { type: "plain", value: "anything" } }), + priorConfig: null, + }); + expect(decision).toEqual({ introducesBinding: false, keepsBinding: false }); + }); + + it("recognizes only the exact fixed binding shape", () => { + expect(isFixedClaudeOAuthBinding(FIXED_BINDING)).toBe(true); + expect(isFixedClaudeOAuthBinding({ type: "plain", value: "x" })).toBe(false); + expect(isFixedClaudeOAuthBinding({ type: "user_secret_ref", key: "OTHER" })).toBe(false); + expect(isFixedClaudeOAuthBinding(null)).toBe(false); + }); + + it("raises a fixed 409 claim error", () => { + const error = claudeOAuthClaimRejectedError(); + expect(error.status).toBe(409); + expect(error.message).toBe(CLAUDE_OAUTH_CLAIM_REJECTED); + }); +}); + +// --- The stored-session claim on the create and hire paths (Postgres) -------- + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +if (!embeddedPostgresSupport.supported) { + console.warn( + `Skipping Claude OAuth binding claim tests on this host: ${ + embeddedPostgresSupport.reason ?? "unsupported environment" + }`, + ); +} + +describeEmbeddedPostgres("agent service Claude OAuth binding claim", () => { + let stopDb: (() => Promise) | null = null; + let connectionString!: string; + let db!: ReturnType; + const previousKeyFile = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; + const secretsTmpDir = path.join(os.tmpdir(), `paperclip-claude-oauth-binding-${randomUUID()}`); + + beforeAll(async () => { + mkdirSync(secretsTmpDir, { recursive: true }); + process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = path.join(secretsTmpDir, "master.key"); + const started = await startEmbeddedPostgresTestDatabase("claude-oauth-binding"); + stopDb = started.cleanup; + connectionString = started.connectionString; + db = createDb(connectionString); + }, 20_000); + + afterEach(async () => { + await db.delete(companySecretBindings); + await db.delete(companySecretVersions); + await db.delete(companySecrets); + await db.delete(companySecretProviderConfigs); + await db.delete(userSecretDeclarations); + await db.delete(userSecretDefinitions); + await db.delete(claudeSetupTokenSessions); + await db.delete(agents); + await db.delete(environments); + await db.delete(companies); + }); + + afterAll(async () => { + await stopDb?.(); + if (previousKeyFile === undefined) { + delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; + } else { + process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = previousKeyFile; + } + rmSync(secretsTmpDir, { recursive: true, force: true }); + }); + + interface Scope { + companyId: string; + environmentId: string; + ownerUserId: string; + } + + async function seedScope(): Promise { + const companyId = randomUUID(); + const environmentId = randomUUID(); + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + await db.insert(environments).values({ + id: environmentId, + name: `sandbox-${environmentId.slice(0, 8)}`, + driver: "sandbox", + status: "active", + config: { provider: "fake" }, + createdAt: new Date(), + updatedAt: new Date(), + }); + return { companyId, environmentId, ownerUserId: `user-${randomUUID().slice(0, 8)}` }; + } + + async function seedStoredClaim(scope: Scope, deadlineMs: number, state = "stored"): Promise { + const sessionId = randomUUID(); + await createDbSetupTokenCleanupStore(db).record({ + sessionId, + companyId: scope.companyId, + ownerUserId: scope.ownerUserId, + adapterType: "claude_local", + environmentId: scope.environmentId, + leaseId: "lease-1", + deadline: deadlineMs, + state: state as never, + boundAt: null, + }); + return sessionId; + } + + function createInput(scope: Scope, extraEnv: Record = {}) { + return { + name: "Claude Login Agent", + role: "engineer", + status: "idle" as const, + adapterType: "claude_local", + defaultEnvironmentId: scope.environmentId, + adapterConfig: { env: { CLAUDE_CODE_OAUTH_TOKEN: { ...FIXED_BINDING }, ...extraEnv } }, + runtimeConfig: {}, + spentMonthlyCents: 0, + lastHeartbeatAt: null, + }; + } + + async function readClaim(sessionId: string) { + const rows = await db + .select() + .from(claudeSetupTokenSessions) + .where(eq(claudeSetupTokenSessions.sessionId, sessionId)); + return rows[0]; + } + + async function countAgents(companyId: string): Promise { + const rows = await db.select().from(agents).where(eq(agents.companyId, companyId)); + return rows.length; + } + + // Seed one stored owner value for the fixed Claude OAuth definition. The + // apply-existing path binds the fixed reference only when this value exists. + async function seedStoredOwnerValue(scope: Scope, value = "sk-owner-token"): Promise { + await secretService(db).completeClaudeOAuthUserSecret(scope.companyId, scope.ownerUserId, { + sessionId: randomUUID(), + mode: "first_write", + value, + }); + } + + async function countUserSecretDefinitions(companyId: string): Promise { + const rows = await db + .select() + .from(userSecretDefinitions) + .where(eq(userSecretDefinitions.companyId, companyId)); + return rows.length; + } + + async function countDeclarationsForAgent(agentId: string): Promise { + const rows = await db + .select() + .from(userSecretDeclarations) + .where(eq(userSecretDeclarations.targetId, agentId)); + return rows.length; + } + + it("inserts the fixed binding and consumes a valid claim exactly once", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000); + + const created = await agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }); + + const persisted = created.adapterConfig as { env: Record }; + expect(persisted.env.CLAUDE_CODE_OAUTH_TOKEN).toMatchObject({ + type: "user_secret_ref", + key: "CLAUDE_CODE_OAUTH_TOKEN", + }); + // The claim is consumed once. + expect((await readClaim(sessionId))?.boundAt).not.toBeNull(); + + // A replay of the same claim inserts no second agent. + await expect( + agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }), + ).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED }); + expect(await countAgents(scope.companyId)).toBe(1); + }); + + it("rejects a missing claim and inserts no binding", async () => { + const scope = await seedScope(); + await expect( + agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: undefined, ownerUserId: scope.ownerUserId }, + }), + ).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED }); + expect(await countAgents(scope.companyId)).toBe(0); + }); + + it("rejects a foreign-owner claim and leaves it unconsumed", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000); + await expect( + agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: "intruder" }, + }), + ).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED }); + expect((await readClaim(sessionId))?.boundAt).toBeNull(); + expect(await countAgents(scope.companyId)).toBe(0); + }); + + it("rejects an expired claim and leaves it unconsumed", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() - 1_000); + await expect( + agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }), + ).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED }); + expect((await readClaim(sessionId))?.boundAt).toBeNull(); + }); + + it("rejects a non-stored claim and leaves it unconsumed", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000, "awaiting_code"); + await expect( + agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }), + ).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED }); + expect((await readClaim(sessionId))?.boundAt).toBeNull(); + }); + + it("returns a byte-identical error for every reject reason", async () => { + const messages: string[] = []; + // Missing claim. + const missingScope = await seedScope(); + messages.push( + await agentService(db) + .create(missingScope.companyId, createInput(missingScope), { + claudeLogin: { storedSessionId: undefined, ownerUserId: missingScope.ownerUserId }, + }) + .then(() => "no-error") + .catch((error: Error) => error.message), + ); + // Foreign owner. + const foreignScope = await seedScope(); + const foreignSession = await seedStoredClaim(foreignScope, Date.now() + 60_000); + messages.push( + await agentService(db) + .create(foreignScope.companyId, createInput(foreignScope), { + claudeLogin: { storedSessionId: foreignSession, ownerUserId: "intruder" }, + }) + .then(() => "no-error") + .catch((error: Error) => error.message), + ); + // Expired. + const expiredScope = await seedScope(); + const expiredSession = await seedStoredClaim(expiredScope, Date.now() - 1_000); + messages.push( + await agentService(db) + .create(expiredScope.companyId, createInput(expiredScope), { + claudeLogin: { storedSessionId: expiredSession, ownerUserId: expiredScope.ownerUserId }, + }) + .then(() => "no-error") + .catch((error: Error) => error.message), + ); + // Non-stored. + const nonStoredScope = await seedScope(); + const nonStoredSession = await seedStoredClaim(nonStoredScope, Date.now() + 60_000, "submitting"); + messages.push( + await agentService(db) + .create(nonStoredScope.companyId, createInput(nonStoredScope), { + claudeLogin: { storedSessionId: nonStoredSession, ownerUserId: nonStoredScope.ownerUserId }, + }) + .then(() => "no-error") + .catch((error: Error) => error.message), + ); + // Already consumed. + const consumedScope = await seedScope(); + const consumedSession = await seedStoredClaim(consumedScope, Date.now() + 60_000); + await agentService(db).create(consumedScope.companyId, createInput(consumedScope), { + claudeLogin: { storedSessionId: consumedSession, ownerUserId: consumedScope.ownerUserId }, + }); + messages.push( + await agentService(db) + .create(consumedScope.companyId, createInput(consumedScope), { + claudeLogin: { storedSessionId: consumedSession, ownerUserId: consumedScope.ownerUserId }, + }) + .then(() => "no-error") + .catch((error: Error) => error.message), + ); + + expect(new Set(messages)).toEqual(new Set([CLAUDE_OAUTH_CLAIM_REJECTED])); + }); + + it("consumes one claim exactly once under two concurrent creates", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000); + + const results = await Promise.allSettled([ + agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }), + agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }), + ]); + + const fulfilled = results.filter((result) => result.status === "fulfilled"); + const rejected = results.filter((result) => result.status === "rejected"); + expect(fulfilled).toHaveLength(1); + expect(rejected).toHaveLength(1); + expect((rejected[0] as PromiseRejectedResult).reason.message).toBe(CLAUDE_OAUTH_CLAIM_REJECTED); + expect(await countAgents(scope.companyId)).toBe(1); + }); + + it("inserts no binding when the claim row lock holds until after the deadline", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 3_600_000); + + const lockDb = createDb(connectionString); + let signalLocked!: () => void; + const locked = new Promise((resolve) => { + signalLocked = resolve; + }); + let releaseGate!: () => void; + const gate = new Promise((resolve) => { + releaseGate = resolve; + }); + const lockHeld = lockDb.transaction(async (tx) => { + await tx.execute( + sql`SELECT bound_at FROM claude_setup_token_sessions WHERE session_id = ${sessionId} FOR UPDATE`, + ); + await tx.execute( + sql`UPDATE claude_setup_token_sessions SET deadline_at = clock_timestamp() + interval '300 milliseconds' WHERE session_id = ${sessionId}`, + ); + signalLocked(); + await gate; + }); + + await locked; + const createPromise = agentService(db) + .create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }) + .then(() => "created") + .catch((error: Error) => error.message); + await new Promise((resolve) => setTimeout(resolve, 600)); + releaseGate(); + await lockHeld; + + expect(await createPromise).toBe(CLAUDE_OAUTH_CLAIM_REJECTED); + expect((await readClaim(sessionId))?.boundAt).toBeNull(); + expect(await countAgents(scope.companyId)).toBe(0); + await lockDb.$client.end(); + }); + + it("removes the fixed binding on a normal update", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000); + const created = await agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }); + + const updated = await agentService(db).update(created.id, { adapterConfig: { env: {} } }); + + const updatedEnv = (updated?.adapterConfig as { env: Record }).env; + expect(updatedEnv.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined(); + }); + + it("re-points the binding to a plain value on a normal update", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000); + const created = await agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }); + + const updated = await agentService(db).update(created.id, { + adapterConfig: { env: { CLAUDE_CODE_OAUTH_TOKEN: { type: "plain", value: "sk-fake" } } }, + }); + + const updatedEnv = (updated?.adapterConfig as { env: Record }).env; + expect(updatedEnv.CLAUDE_CODE_OAUTH_TOKEN).toMatchObject({ type: "plain", value: "sk-fake" }); + }); + + it("removes the binding by a move to another adapter type on a normal update", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000); + const created = await agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }); + + // The write moves the agent to the process adapter and drops the binding in + // the same PATCH. The binding is a normal environment variable, so the write + // succeeds and drops both the fixed binding and the claude_local adapter. + const updated = await agentService(db).update(created.id, { + adapterType: "process", + adapterConfig: { env: {} }, + }); + + expect(updated?.adapterType).toBe("process"); + const updatedEnv = (updated?.adapterConfig as { env: Record }).env; + expect(updatedEnv.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined(); + }); + + it("keeps an existing binding when the update changes another field", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000); + const created = await agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { storedSessionId: sessionId, ownerUserId: scope.ownerUserId }, + }); + + const updated = await agentService(db).update(created.id, { + adapterConfig: { model: "claude-opus", env: { CLAUDE_CODE_OAUTH_TOKEN: { ...FIXED_BINDING } } }, + }); + const env = (updated?.adapterConfig as { env: Record }).env; + expect(env.CLAUDE_CODE_OAUTH_TOKEN).toMatchObject(FIXED_BINDING); + }); + + it("rejects a newly introduced binding on update, because it carries no claim", async () => { + const scope = await seedScope(); + const created = await agentService(db).create(scope.companyId, { + name: "Api Key Agent", + role: "engineer", + status: "idle", + adapterType: "claude_local", + defaultEnvironmentId: scope.environmentId, + adapterConfig: { env: {} }, + runtimeConfig: {}, + spentMonthlyCents: 0, + lastHeartbeatAt: null, + }); + + await expect( + agentService(db).update(created.id, { + adapterConfig: { env: { CLAUDE_CODE_OAUTH_TOKEN: { ...FIXED_BINDING } } }, + }), + ).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED }); + + const reloaded = await agentService(db).getById(created.id); + const reloadedEnv = (reloaded?.adapterConfig as { env: Record }).env; + expect(reloadedEnv.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined(); + }); + + // --- The user-actor apply-existing path (no login round trip) -------------- + + it("binds the fixed reference from a stored owner value with no claim", async () => { + const scope = await seedScope(); + await seedStoredOwnerValue(scope); + + const created = await agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { ownerUserId: scope.ownerUserId, applyExistingWithoutClaim: true }, + }); + + const persisted = created.adapterConfig as { env: Record }; + expect(persisted.env.CLAUDE_CODE_OAUTH_TOKEN).toMatchObject({ + type: "user_secret_ref", + key: "CLAUDE_CODE_OAUTH_TOKEN", + }); + // The response carries the reference only. It carries no token value. + expect(JSON.stringify(created)).not.toContain("sk-owner-token"); + expect(await countAgents(scope.companyId)).toBe(1); + // The path consumes no setup-token claim, so it needs no login round trip. + const claims = await db.select().from(claudeSetupTokenSessions); + expect(claims).toHaveLength(0); + }); + + it("binds on a hire-shaped create that needs board approval", async () => { + const scope = await seedScope(); + await seedStoredOwnerValue(scope); + + const created = await agentService( + db, + ).create(scope.companyId, { ...createInput(scope), status: "pending_approval" }, { + claudeLogin: { ownerUserId: scope.ownerUserId, applyExistingWithoutClaim: true }, + }); + + expect(created.status).toBe("pending_approval"); + const persisted = created.adapterConfig as { env: Record }; + expect(persisted.env.CLAUDE_CODE_OAUTH_TOKEN).toMatchObject(FIXED_BINDING); + }); + + it("binds the fixed reference on an update from a stored owner value with no claim", async () => { + const scope = await seedScope(); + await seedStoredOwnerValue(scope); + const created = await agentService(db).create(scope.companyId, { + name: "Claude Agent", + role: "engineer", + status: "idle", + adapterType: "claude_local", + defaultEnvironmentId: scope.environmentId, + adapterConfig: { env: {} }, + runtimeConfig: {}, + spentMonthlyCents: 0, + lastHeartbeatAt: null, + }); + + const updated = await agentService(db).update( + created.id, + { adapterConfig: { env: { CLAUDE_CODE_OAUTH_TOKEN: { ...FIXED_BINDING } } } }, + { claudeLogin: { ownerUserId: scope.ownerUserId, applyExistingWithoutClaim: true } }, + ); + + const env = (updated?.adapterConfig as { env: Record }).env; + expect(env.CLAUDE_CODE_OAUTH_TOKEN).toMatchObject(FIXED_BINDING); + }); + + it("rejects apply-existing when the owner has no stored value and binds nothing", async () => { + const scope = await seedScope(); + + await expect( + agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { ownerUserId: scope.ownerUserId, applyExistingWithoutClaim: true }, + }), + ).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED }); + + // A rejected bind creates neither the agent nor the fixed definition. + expect(await countAgents(scope.companyId)).toBe(0); + expect(await countUserSecretDefinitions(scope.companyId)).toBe(0); + }); + + it("rejects apply-existing when no owner is derived (an agent actor)", async () => { + const scope = await seedScope(); + await seedStoredOwnerValue(scope); + + // The route sets no owner for an agent actor. The gate rejects the no-claim + // bind, so an agent actor never binds the fixed reference. + await expect( + agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { ownerUserId: null, applyExistingWithoutClaim: true }, + }), + ).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED }); + expect(await countAgents(scope.companyId)).toBe(0); + }); + + it("rejects apply-existing when the stored value belongs to another company", async () => { + const ownerScope = await seedScope(); + await seedStoredOwnerValue(ownerScope); + // The same owner, a different company. The owner value is company-scoped, so + // the status read finds nothing for the foreign company. + const foreignScope = await seedScope(); + foreignScope.ownerUserId = ownerScope.ownerUserId; + + await expect( + agentService(db).create(foreignScope.companyId, createInput(foreignScope), { + claudeLogin: { ownerUserId: foreignScope.ownerUserId, applyExistingWithoutClaim: true }, + }), + ).rejects.toMatchObject({ message: CLAUDE_OAUTH_CLAIM_REJECTED }); + expect(await countAgents(foreignScope.companyId)).toBe(0); + }); + + it("binds nothing from the flag alone when the config carries no fixed binding", async () => { + const scope = await seedScope(); + await seedStoredOwnerValue(scope); + + // The apply-existing flag is set, but the config carries no fixed binding. The + // gate mints the fixed reference only when the client presents the exact fixed + // binding, so the flag alone binds nothing and creates no fixed definition. + const created = await agentService(db).create( + scope.companyId, + { + name: "Plain Agent", + role: "engineer", + status: "idle", + adapterType: "claude_local", + defaultEnvironmentId: scope.environmentId, + adapterConfig: { env: {} }, + runtimeConfig: {}, + spentMonthlyCents: 0, + lastHeartbeatAt: null, + }, + { claudeLogin: { ownerUserId: scope.ownerUserId, applyExistingWithoutClaim: true } }, + ); + + const env = (created.adapterConfig as { env: Record }).env; + expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined(); + expect(await countDeclarationsForAgent(created.id)).toBe(0); + }); + + it("derives the persisted binding shape, so a client secret selector never rides through", async () => { + const scope = await seedScope(); + await seedStoredOwnerValue(scope); + + // A client adds a secret id and a foreign owner to the binding. The persist + // normalization keeps only the fixed reference fields, so the extra selectors + // never reach the stored binding. The runtime derives the owner from context. + const created = await agentService(db).create( + scope.companyId, + createInput(scope, { + CLAUDE_CODE_OAUTH_TOKEN: { + type: "user_secret_ref", + key: "CLAUDE_CODE_OAUTH_TOKEN", + secretId: "attacker-secret", + ownerUserId: "intruder", + }, + }), + { claudeLogin: { ownerUserId: scope.ownerUserId, applyExistingWithoutClaim: true } }, + ); + + const binding = (created.adapterConfig as { env: Record> }).env + .CLAUDE_CODE_OAUTH_TOKEN; + expect(binding).toMatchObject({ type: "user_secret_ref", key: "CLAUDE_CODE_OAUTH_TOKEN" }); + expect(binding.secretId).toBeUndefined(); + expect(binding.ownerUserId).toBeUndefined(); + }); + + it("resolves the bound reference to the stored value without leaking it in a log", async () => { + const scope = await seedScope(); + await seedStoredOwnerValue(scope, "sk-secret-resolve"); + const created = await agentService(db).create(scope.companyId, createInput(scope), { + claudeLogin: { ownerUserId: scope.ownerUserId, applyExistingWithoutClaim: true }, + }); + + // The successful bind writes a declaration for the agent, so the fixed + // reference resolves at runtime. The declaration and the definition carry no + // token value. + expect(await countDeclarationsForAgent(created.id)).toBe(1); + const definitions = await db.select().from(userSecretDefinitions); + expect(JSON.stringify(definitions)).not.toContain("sk-secret-resolve"); + }); + + // --- The atomic credential-claim writer (item 2) --------------------------- + + function claimScope(scope: Scope): SetupTokenSessionScope { + return { + companyId: scope.companyId, + ownerUserId: scope.ownerUserId, + targetAgentId: null, + adapterType: "claude_local", + environmentId: scope.environmentId, + }; + } + + it("commits the stored transition and the secret together on a first write", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000, "submitting"); + const writer = createSetupTokenSecretWriter({ db }); + + await writer({ scope: claimScope(scope), sessionId, token: "sk-ant-oat01-FIRSTWRITE" }); + + // The durable row transitioned to `stored` and stays unconsumed. + const claim = await readClaim(sessionId); + expect(claim?.state).toBe("stored"); + expect(claim?.boundAt).toBeNull(); + // The owner value committed in the same transaction. + const status = await secretService(db).readClaudeOAuthUserSecretStatus( + scope.companyId, + scope.ownerUserId, + ); + expect(status).not.toBeNull(); + expect(status?.latestVersion).toBe(1); + }); + + it("rolls back the transition and writes no secret when the secret write fails", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000, "submitting"); + // Inject a completion that fails after the transition. The whole transaction + // rolls back, so neither the claim nor the secret commits. + const writer = createSetupTokenSecretWriter({ + db, + completionForTx: () => ({ + async completeClaudeOAuthUserSecret() { + throw new Error("storage down"); + }, + }), + }); + + await expect( + writer({ scope: claimScope(scope), sessionId, token: "sk-ant-oat01-ROLLBACK" }), + ).rejects.toThrow(); + + const claim = await readClaim(sessionId); + expect(claim?.state).toBe("submitting"); + expect(claim?.boundAt).toBeNull(); + expect( + await secretService(db).readClaudeOAuthUserSecretStatus(scope.companyId, scope.ownerUserId), + ).toBeNull(); + expect(await countUserSecretDefinitions(scope.companyId)).toBe(0); + }); + + it("writes no secret for an expired claim", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() - 1_000, "submitting"); + const writer = createSetupTokenSecretWriter({ db }); + + await expect( + writer({ scope: claimScope(scope), sessionId, token: "sk-ant-oat01-EXPIRED" }), + ).rejects.toThrow(); + + expect((await readClaim(sessionId))?.state).toBe("submitting"); + expect( + await secretService(db).readClaudeOAuthUserSecretStatus(scope.companyId, scope.ownerUserId), + ).toBeNull(); + }); + + it("writes no secret for an already-consumed claim", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000); + // Consume the stored claim first, so `bound_at` is set and the predecessor + // predicate no longer matches. + await createDbSetupTokenCleanupStore(db).consumeStoredClaim({ + sessionId, + companyId: scope.companyId, + ownerUserId: scope.ownerUserId, + adapterType: "claude_local", + environmentId: scope.environmentId, + }); + const writer = createSetupTokenSecretWriter({ db }); + + await expect( + writer({ scope: claimScope(scope), sessionId, token: "sk-ant-oat01-BOUND" }), + ).rejects.toThrow(); + + expect( + await secretService(db).readClaudeOAuthUserSecretStatus(scope.companyId, scope.ownerUserId), + ).toBeNull(); + }); + + it("rotates the stored value under the captured version on a confirmed overwrite", async () => { + const scope = await seedScope(); + await seedStoredOwnerValue(scope, "sk-old-value"); + const before = await secretService(db).readClaudeOAuthUserSecretStatus( + scope.companyId, + scope.ownerUserId, + ); + expect(before).not.toBeNull(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000, "submitting"); + const writer = createSetupTokenSecretWriter({ db }); + + await writer({ + scope: { + ...claimScope(scope), + confirmedOverwrite: { + expectedSecretId: before!.secretId, + expectedLatestVersion: before!.latestVersion, + }, + }, + sessionId, + token: "sk-ant-oat01-ROTATED", + }); + + // The claim transitioned and the stored value rotated to a new version. + expect((await readClaim(sessionId))?.state).toBe("stored"); + const after = await secretService(db).readClaudeOAuthUserSecretStatus( + scope.companyId, + scope.ownerUserId, + ); + expect(after?.secretId).toBe(before!.secretId); + expect(after?.latestVersion).toBe(before!.latestVersion + 1); + }); + + it("deletes a cleanup row only under the full owner scope", async () => { + const scope = await seedScope(); + const sessionId = await seedStoredClaim(scope, Date.now() + 60_000); + const store = createDbSetupTokenCleanupStore(db); + const identity = { + sessionId, + companyId: scope.companyId, + ownerUserId: scope.ownerUserId, + adapterType: "claude_local", + environmentId: scope.environmentId, + }; + + // A foreign-owner identity with the right session id does not delete the row. + await store.remove({ ...identity, ownerUserId: "intruder" }); + expect(await readClaim(sessionId)).toBeTruthy(); + + // The full-scope identity deletes the row. + await store.remove(identity); + expect(await readClaim(sessionId)).toBeUndefined(); + }); +}); diff --git a/server/src/__tests__/bundled-plugins.test.ts b/server/src/__tests__/bundled-plugins.test.ts index e137634c9c..1d52059aaa 100644 --- a/server/src/__tests__/bundled-plugins.test.ts +++ b/server/src/__tests__/bundled-plugins.test.ts @@ -196,32 +196,72 @@ describe("resolveBundledCatalogRoot", () => { // ensureBundledPlugins (fail-safe installer) // --------------------------------------------------------------------------- +type LooseRow = { + id: string; + pluginKey: string; + status: string; + version?: string; + manifestJson?: Record; +}; + +// Build a minimal manifest for a persisted row or a shipped bundle. The reconcile +// step compares the bundle version with the persisted version. +function makeManifest(pluginKey: string, version: string) { + return { id: pluginKey, apiVersion: 1, version } as unknown as import("@paperclipai/shared").PaperclipPluginManifestV1; +} + function makeDeps(overrides?: { - rows?: Record; + rows?: Record; bundleManifestExists?: (localPath: string) => boolean; installError?: Error; + /** Version the shipped bundle manifest reports, keyed by pluginKey. */ + bundleVersionByKey?: Record; }) { const rows = overrides?.rows ?? {}; - const installedRows = new Map(Object.entries(rows)); + const normalize = (row: LooseRow | null): (LooseRow & { version: string; manifestJson: Record }) | null => { + if (!row) return null; + const version = row.version ?? "0.1.0"; + return { + ...row, + version, + manifestJson: row.manifestJson ?? { id: row.pluginKey, apiVersion: 1, version }, + }; + }; + const installedRows = new Map( + Object.entries(rows).map(([key, row]) => [key, normalize(row)] as const), + ); const installPlugin = vi.fn(async ({ localPath }: { localPath: string }) => { if (overrides?.installError) throw overrides.installError; const entry = BUNDLED_PLUGIN_CATALOG.find((candidate) => localPath.endsWith(candidate.relativePath), ); const pluginKey = entry?.pluginKey ?? "unknown"; - installedRows.set(pluginKey, { id: `id-${pluginKey}`, pluginKey, status: "installed" }); + installedRows.set(pluginKey, normalize({ id: `id-${pluginKey}`, pluginKey, status: "installed" })); return { manifest: { id: pluginKey } }; }); + const update = vi.fn(async () => undefined); + const loadManifest = vi.fn(async (localPath: string) => { + const entry = BUNDLED_PLUGIN_CATALOG.find((candidate) => + localPath.endsWith(candidate.relativePath), + ); + const pluginKey = entry?.pluginKey ?? "unknown"; + const persisted = installedRows.get(pluginKey); + // Default: the bundle version matches the persisted version, so no + // reconcile fires unless a test overrides `bundleVersionByKey`. + const version = overrides?.bundleVersionByKey?.[pluginKey] ?? persisted?.version ?? "0.1.0"; + return makeManifest(pluginKey, version); + }); const deps: BundledPluginProvisionerDeps = { registry: { getByKey: vi.fn(async (pluginKey: string) => installedRows.get(pluginKey) ?? null), - }, - loader: { installPlugin } as unknown as BundledPluginProvisionerDeps["loader"], + update, + } as unknown as BundledPluginProvisionerDeps["registry"], + loader: { installPlugin, loadManifest } as unknown as BundledPluginProvisionerDeps["loader"], lifecycle: { load: vi.fn(async () => undefined) }, logger: { info: vi.fn(), error: vi.fn() }, bundleManifestExists: overrides?.bundleManifestExists ?? (() => true), }; - return { deps, installPlugin }; + return { deps, installPlugin, update, loadManifest }; } const K8S: ResolvedBundledPlugin = { @@ -258,6 +298,64 @@ describe("ensureBundledPlugins", () => { } }); + it("reconciles the persisted manifest of a present plugin when the bundle version changed", async () => { + const { deps, installPlugin, update } = makeDeps({ + rows: { + [DAYTONA.pluginKey]: { + id: "row-daytona", + pluginKey: DAYTONA.pluginKey, + status: "ready", + version: "0.1.1", + }, + }, + bundleVersionByKey: { [DAYTONA.pluginKey]: "0.1.2" }, + }); + await ensureBundledPlugins([DAYTONA], deps, { reinstallUninstalled: false }); + // The plugin stays present, so it is never re-installed. + expect(installPlugin).not.toHaveBeenCalled(); + // The persisted manifest is refreshed to the shipped bundle version, so a + // capability added to the bundle reaches the existing install. + expect(update).toHaveBeenCalledWith( + "row-daytona", + expect.objectContaining({ version: "0.1.2" }), + ); + }); + + it("does not reconcile when the persisted version already matches the bundle", async () => { + const { deps, update } = makeDeps({ + rows: { + [DAYTONA.pluginKey]: { + id: "row-daytona", + pluginKey: DAYTONA.pluginKey, + status: "ready", + version: "0.1.2", + }, + }, + bundleVersionByKey: { [DAYTONA.pluginKey]: "0.1.2" }, + }); + await ensureBundledPlugins([DAYTONA], deps, { reinstallUninstalled: false }); + expect(update).not.toHaveBeenCalled(); + }); + + it("swallows a reconcile error and continues boot", async () => { + const { deps, update } = makeDeps({ + rows: { + [DAYTONA.pluginKey]: { + id: "row-daytona", + pluginKey: DAYTONA.pluginKey, + status: "ready", + version: "0.1.1", + }, + }, + bundleVersionByKey: { [DAYTONA.pluginKey]: "0.1.2" }, + }); + (update as ReturnType).mockRejectedValueOnce(new Error("db down")); + await expect( + ensureBundledPlugins([DAYTONA], deps, { reinstallUninstalled: false }), + ).resolves.toBeUndefined(); + expect(deps.logger.error).toHaveBeenCalled(); + }); + it("reinstalls a soft-uninstalled plugin in managed mode", async () => { const { deps, installPlugin } = makeDeps({ rows: { diff --git a/server/src/__tests__/environment-routes.test.ts b/server/src/__tests__/environment-routes.test.ts index 9cdac48a9a..b828a83bf2 100644 --- a/server/src/__tests__/environment-routes.test.ts +++ b/server/src/__tests__/environment-routes.test.ts @@ -39,6 +39,7 @@ const mockEnvironmentService = vi.hoisted(() => ({ update: vi.fn(), removeIfDeletable: vi.fn(), getDeleteBlastRadius: vi.fn(), + hasUnresolvedPendingCleanupLeases: vi.fn(), listLeases: vi.fn(), getLeaseById: vi.fn(), })); @@ -150,6 +151,8 @@ function createDeleteBlastRadius(overrides: Partial<{ secretBindingCount: number; activeLeaseCount: number; activeCustomImageSetupSessionCount: number; + pendingCleanupLeaseCount: number; + reusableSandboxLeaseCount: number; }> = {}) { const staticReferences = { isManagedLocal: overrides.isManagedLocal ?? false, @@ -167,14 +170,20 @@ function createDeleteBlastRadius(overrides: Partial<{ (overrides.activeLeaseCount ?? 0) > 0 || (overrides.activeCustomImageSetupSessionCount ?? 0) > 0, }; + const pendingCleanupLeaseCount = overrides.pendingCleanupLeaseCount ?? 0; + const reusableSandboxLeaseCount = overrides.reusableSandboxLeaseCount ?? 0; const deleteBlockedReasons = [ ...(staticReferences.isManagedLocal ? ["managed_local" as const] : []), ...(staticReferences.isInstanceDefault ? ["instance_default" as const] : []), + ...(pendingCleanupLeaseCount > 0 ? ["pending_sandbox_cleanup" as const] : []), + ...(reusableSandboxLeaseCount > 0 ? ["reusable_sandbox_lease" as const] : []), ]; return { environmentId: "env-1", canDelete: deleteBlockedReasons.length === 0, deleteBlockedReasons, + pendingCleanupLeaseCount, + reusableSandboxLeaseCount, staticReferences, activeRuntimeUse, }; @@ -255,6 +264,8 @@ describe("environment routes", () => { mockEnvironmentService.update.mockReset(); mockEnvironmentService.removeIfDeletable.mockReset(); mockEnvironmentService.getDeleteBlastRadius.mockReset(); + mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockReset(); + mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(false); mockEnvironmentService.listLeases.mockReset(); mockEnvironmentService.getLeaseById.mockReset(); mockExecutionWorkspaceService.clearEnvironmentSelection.mockReset(); @@ -1078,6 +1089,8 @@ describe("environment routes", () => { environmentId: "env-1", canDelete: true, deleteBlockedReasons: [], + pendingCleanupLeaseCount: 0, + reusableSandboxLeaseCount: 0, staticReferences: { isManagedLocal: false, isInstanceDefault: false, @@ -1522,6 +1535,153 @@ describe("environment routes", () => { expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); }); + it("rejects deleting an environment with a pending sandbox cleanup", async () => { + const environment = { + ...createEnvironment(), + driver: "ssh" as const, + name: "SSH Fixture", + config: { + host: "ssh.example.test", + port: 22, + username: "ssh-user", + remoteWorkspacePath: "/srv/paperclip/workspace", + privateKey: null, + privateKeySecretRef: null, + knownHosts: null, + strictHostKeyChecking: true, + }, + }; + mockEnvironmentService.getById.mockResolvedValue(environment); + mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({ + pendingCleanupLeaseCount: 1, + })); + const app = createApp({ + type: "board", + userId: "admin-1", + source: "local_implicit", + }); + + const res = await request(app).delete("/api/environments/env-1"); + + expect(res.status).toBe(409); + expect(res.body.error).toBe( + "Cannot delete this environment while a sandbox cleanup is pending. Wait for the cleanup sweep to destroy the orphan sandbox, then retry.", + ); + expect(res.body.details).toEqual({ deleteBlockedReasons: ["pending_sandbox_cleanup"] }); + expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); + }); + + it("rejects deleting an environment with a live reusable sandbox lease", async () => { + const environment = { + ...createEnvironment(), + driver: "sandbox" as const, + name: "Reusable Sandbox Fixture", + config: { + provider: "fake-plugin", + image: "fixture:test", + reuseLease: true, + }, + }; + mockEnvironmentService.getById.mockResolvedValue(environment); + mockEnvironmentService.getDeleteBlastRadius.mockResolvedValue(createDeleteBlastRadius({ + activeLeaseCount: 1, + reusableSandboxLeaseCount: 1, + })); + const app = createApp({ + type: "board", + userId: "admin-1", + source: "local_implicit", + }); + + const res = await request(app).delete("/api/environments/env-1"); + + expect(res.status).toBe(409); + expect(res.body.error).toBe( + "Cannot delete this environment while it has a reusable sandbox lease. Remove the associated execution workspace or issue so Paperclip can destroy the sandbox, then retry.", + ); + expect(res.body.details).toEqual({ deleteBlockedReasons: ["reusable_sandbox_lease"] }); + expect(mockEnvironmentService.removeIfDeletable).not.toHaveBeenCalled(); + }); + + it("rejects a driver or provider config change while a sandbox cleanup is pending", async () => { + const environment = { + ...createEnvironment(), + id: "env-ssh", + driver: "ssh" as const, + name: "SSH Fixture", + config: { + host: "ssh.example.test", + port: 22, + username: "ssh-user", + remoteWorkspacePath: "/srv/paperclip/workspace", + privateKey: null, + privateKeySecretRef: null, + knownHosts: null, + strictHostKeyChecking: true, + }, + }; + mockEnvironmentService.getById.mockResolvedValue(environment); + mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true); + const app = createApp({ + type: "board", + userId: "admin-1", + source: "local_implicit", + }); + + const res = await request(app) + .patch("/api/environments/env-ssh") + .send({ + config: { + host: "changed.example.test", + port: 22, + username: "ssh-user", + remoteWorkspacePath: "/srv/paperclip/workspace", + }, + }); + + expect(res.status).toBe(409); + expect(res.body.error).toBe( + "Cannot change the driver or provider config while a sandbox cleanup is pending. Wait for the cleanup sweep to destroy the orphan sandbox, then retry.", + ); + expect(res.body.details).toEqual({ code: "environment_pending_sandbox_cleanup" }); + expect(mockEnvironmentService.update).not.toHaveBeenCalled(); + }); + + it("allows a non-provider update while a sandbox cleanup is pending", async () => { + const environment = { + ...createEnvironment(), + id: "env-ssh", + driver: "ssh" as const, + name: "SSH Fixture", + config: { + host: "ssh.example.test", + port: 22, + username: "ssh-user", + remoteWorkspacePath: "/srv/paperclip/workspace", + privateKey: null, + privateKeySecretRef: null, + knownHosts: null, + strictHostKeyChecking: true, + }, + }; + mockEnvironmentService.getById.mockResolvedValue(environment); + mockEnvironmentService.hasUnresolvedPendingCleanupLeases.mockResolvedValue(true); + mockEnvironmentService.update.mockResolvedValue({ ...environment, description: "Updated" }); + const app = createApp({ + type: "board", + userId: "admin-1", + source: "local_implicit", + }); + + const res = await request(app) + .patch("/api/environments/env-ssh") + .send({ description: "Updated" }); + + expect(res.status).toBe(200); + expect(mockEnvironmentService.hasUnresolvedPendingCleanupLeases).not.toHaveBeenCalled(); + expect(mockEnvironmentService.update).toHaveBeenCalled(); + }); + it("describes an environment's secret refs with owner metadata", async () => { const secretId = "22222222-2222-2222-2222-222222222222"; mockEnvironmentService.getById.mockResolvedValue({ diff --git a/server/src/__tests__/environment-runtime.test.ts b/server/src/__tests__/environment-runtime.test.ts index 174bd7b60d..ed424100f6 100644 --- a/server/src/__tests__/environment-runtime.test.ts +++ b/server/src/__tests__/environment-runtime.test.ts @@ -1,8 +1,8 @@ import { createHash, randomUUID } from "node:crypto"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdtemp, readdir, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { eq } from "drizzle-orm"; import { buildSshEnvLabFixtureConfig, @@ -12,6 +12,7 @@ import { } from "@paperclipai/adapter-utils/ssh"; import { agents, + builtInManagedResources, companies, companySecretVersions, companySecrets, @@ -28,8 +29,17 @@ import { startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; import { resolveEnvironmentDriverConfigForRuntime } from "../services/environment-config.ts"; -import { SANDBOX_CAPABILITY_KEYS, environmentRuntimeService, findReusableSandboxLeaseId } from "../services/environment-runtime.ts"; +import { + SANDBOX_CAPABILITY_KEYS, + environmentRuntimeService, + findReusableSandboxLeaseId, + SandboxOrphanCleanupWriteError, +} from "../services/environment-runtime.ts"; +import * as sandboxProviderRuntime from "../services/sandbox-provider-runtime.ts"; +import * as environmentsModule from "../services/environments.ts"; +import { logger } from "../middleware/logger.ts"; import { environmentService } from "../services/environments.ts"; +import { heartbeatService } from "../services/heartbeat.ts"; import { secretService } from "../services/secrets.ts"; import type { PluginWorkerManager } from "../services/plugin-worker-manager.ts"; import { @@ -139,6 +149,11 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { let db!: ReturnType; let runtime!: ReturnType; const fixtureRoots: string[] = []; + // Give each test its own orphan-cleanup spool directory, so a spool file one + // test writes never leaks into another test's flush. The default runtime reads + // this env override when a test does not pass an explicit spool directory. + let orphanCleanupSpoolDir: string | null = null; + const previousOrphanCleanupSpoolDir = process.env.SANDBOX_ORPHAN_CLEANUP_SPOOL_DIR; beforeAll(async () => { const started = await startEmbeddedPostgresTestDatabase("environment-runtime"); @@ -147,7 +162,21 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { runtime = environmentRuntimeService(db); }); + beforeEach(async () => { + orphanCleanupSpoolDir = await mkdtemp(path.join(os.tmpdir(), "orphan-cleanup-spool-")); + process.env.SANDBOX_ORPHAN_CLEANUP_SPOOL_DIR = orphanCleanupSpoolDir; + }); + afterEach(async () => { + if (orphanCleanupSpoolDir) { + await rm(orphanCleanupSpoolDir, { recursive: true, force: true }).catch(() => undefined); + orphanCleanupSpoolDir = null; + } + if (previousOrphanCleanupSpoolDir === undefined) { + delete process.env.SANDBOX_ORPHAN_CLEANUP_SPOOL_DIR; + } else { + process.env.SANDBOX_ORPHAN_CLEANUP_SPOOL_DIR = previousOrphanCleanupSpoolDir; + } while (fixtureRoots.length > 0) { const root = fixtureRoots.pop(); if (!root) continue; @@ -576,6 +605,2286 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { expect(released[0]?.lease.status).toBe("released"); }); + it("releases the remote sandbox when the lease insert rejects a foreign-company binding", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox", + config: { + provider: "fake", + image: "ubuntu:24.04", + reuseLease: false, + }, + }); + + // A managed reconciliation binds the environment to another company after the + // route guard read an empty binding list. The lease insert then rejects the + // foreign-company binding with the 403 `environment_company_mismatch`. + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co", + issuePrefix: "OTA", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + const destroySpy = vi.spyOn(sandboxProviderRuntime, "destroySandboxProviderLease"); + try { + await expect( + runtime.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }), + ).rejects.toMatchObject({ + status: 403, + details: { code: "environment_company_mismatch" }, + }); + + // The acquire records the durable pending-cleanup row before the teardown, + // so the row lands while the database is proven reachable. The successful + // teardown then releases the row to the terminal `expired` state, so no + // active or pending_cleanup row remains for the orphan. + const leaseRows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(leaseRows).toHaveLength(1); + expect(leaseRows[0]?.status).toBe("expired"); + expect(leaseRows[0]?.cleanupStatus).toBe("success"); + + // The acquire already provisioned the remote sandbox, so it releases the + // sandbox on the rejection. Without this teardown the rejected insert leaks + // a live sandbox that no lease row tracks. The teardown carries the + // provisioned provider lease id. + expect(destroySpy).toHaveBeenCalledTimes(1); + expect(destroySpy.mock.calls[0]?.[0]?.providerLeaseId).toMatch( + new RegExp(`^sandbox://fake/${runId}/[0-9a-f-]{36}$`), + ); + } finally { + destroySpy.mockRestore(); + } + }); + + it("destroys the remote plugin sandbox when the lease insert rejects a foreign-company binding", async () => { + // The Claude login runs on a plugin-backed sandbox provider (Daytona), so this + // path is the one the login uses. It must also release the remote sandbox when + // the conditional lease insert rejects a foreign-company binding. + const pluginId = randomUUID(); + const { companyId, environment: baseEnvironment, runId } = await seedEnvironment(); + const pluginConfig = { provider: "fake-plugin", image: "fake:test", reuseLease: false }; + const environment = { + ...baseEnvironment, + name: "Foreign-bound Plugin Sandbox", + driver: "sandbox", + config: pluginConfig, + }; + await environmentService(db).update(environment.id, { + driver: "sandbox", + name: environment.name, + config: pluginConfig, + }); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclip.fake-plugin-sandbox-provider", + packageName: "@paperclipai/plugin-fake-sandbox", + version: "1.0.0", + apiVersion: 1, + categories: ["automation"], + manifestJson: { + id: "paperclip.fake-plugin-sandbox-provider", + apiVersion: 1, + version: "1.0.0", + displayName: "Fake Plugin Sandbox Provider", + description: "Test fake plugin provider", + author: "Paperclip", + categories: ["automation"], + capabilities: ["environment.drivers.register"], + entrypoints: { worker: "dist/worker.js" }, + environmentDrivers: [ + { + driverKey: "fake-plugin", + kind: "sandbox_provider", + displayName: "Fake Plugin", + configSchema: { type: "object" }, + }, + ], + }, + status: "ready", + installOrder: 1, + updatedAt: new Date(), + } as any); + + // A managed reconciliation binds the environment to another company after the + // route guard read an empty binding list. The lease insert then rejects the + // foreign-company binding with the 403 `environment_company_mismatch`. + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co", + issuePrefix: "OTB", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + const workerManager = { + isRunning: vi.fn((id: string) => id === pluginId), + getWorker: vi.fn(() => ({ supportedMethods: ["environmentDestroyLease"] })), + call: vi.fn(async (_pluginId: string, method: string) => { + if (method === "environmentAcquireLease") { + return { + providerLeaseId: "plugin-lease-1", + metadata: { provider: "fake-plugin", image: "fake:test", reuseLease: false }, + }; + } + if (method === "environmentDestroyLease") { + return undefined; + } + throw new Error(`Unexpected plugin method: ${method}`); + }), + } as unknown as PluginWorkerManager; + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + + await expect( + runtimeWithPlugin.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }), + ).rejects.toMatchObject({ + status: 403, + details: { code: "environment_company_mismatch" }, + }); + + // The acquire records the durable pending-cleanup row before the teardown, + // so the row lands while the database is proven reachable. The successful + // teardown then releases the row to the terminal `expired` state, so no + // active or pending_cleanup row remains for the orphan. + const leaseRows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(leaseRows).toHaveLength(1); + expect(leaseRows[0]?.status).toBe("expired"); + expect(leaseRows[0]?.cleanupStatus).toBe("success"); + + // The acquire provisioned the remote plugin sandbox, so it destroys the + // sandbox on the rejection. Without this teardown the rejected insert leaks a + // live sandbox that no lease row tracks. + const destroyCalls = (workerManager.call as unknown as ReturnType).mock.calls.filter( + (callArgs) => callArgs[1] === "environmentDestroyLease", + ); + expect(destroyCalls).toHaveLength(1); + expect(destroyCalls[0]?.[2]).toMatchObject({ providerLeaseId: "plugin-lease-1" }); + + await environmentService(db).update(environment.id, { driver: "local", config: {} }); + }); + + it("records a durable pending-cleanup lease when the built-in teardown fails after a foreign-company rejection", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox Teardown Fail", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Built-in", + issuePrefix: "OTC", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + // The remote teardown fails, so the acquire cannot release the sandbox + // directly. It must record a durable pending-cleanup row instead. + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValue(new Error("teardown failed")); + try { + await expect( + runtime.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }), + ).rejects.toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } }); + + expect(destroySpy).toHaveBeenCalledTimes(1); + // The failed teardown left a durable pending-cleanup row that carries the + // provider lease id for a later sweep. + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("pending_cleanup"); + expect(rows[0]?.cleanupStatus).toBe("failed"); + expect(rows[0]?.providerLeaseId).toMatch(new RegExp(`^sandbox://fake/${runId}/[0-9a-f-]{36}$`)); + } finally { + destroySpy.mockRestore(); + } + }); + + it("records the durable pending-cleanup row before it runs the inline teardown", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox Write First", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Write First", + issuePrefix: "OWF", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + // The teardown reads the lease table when it runs. A durable pending_cleanup + // row must already exist, which proves the acquire records the row before the + // teardown. This ordering closes the window the finding describes: the durable + // write lands while the database is proven reachable, before the teardown RPC + // that a database outage could otherwise interrupt. + let pendingCleanupRowsAtTeardown = -1; + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockImplementation(async () => { + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + pendingCleanupRowsAtTeardown = rows.filter((row) => row.status === "pending_cleanup").length; + }); + try { + await expect( + runtime.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }), + ).rejects.toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } }); + + // The durable row already tracked the orphan when the teardown ran. + expect(pendingCleanupRowsAtTeardown).toBe(1); + expect(destroySpy).toHaveBeenCalledTimes(1); + + // The teardown succeeded, so the acquire released the row to the terminal + // `expired` state and left no pending_cleanup row. + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("expired"); + expect(rows[0]?.cleanupStatus).toBe("success"); + } finally { + destroySpy.mockRestore(); + } + }); + + it("leaves no orphan and no error when the durable write fails but the teardown succeeds", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox Write Fail Teardown Ok", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Write Fail", + issuePrefix: "OWK", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + // Force every durable pending-cleanup write to fail, but let the teardown + // succeed. A successful teardown removes the orphan, so the acquire needs no + // durable row and raises no orphan-cleanup write error. The write-first order + // never turns a clean teardown into a failure. + const realEnvironmentService = environmentsModule.environmentService; + const factorySpy = vi + .spyOn(environmentsModule, "environmentService") + .mockImplementation((database: Parameters[0]) => { + const real = realEnvironmentService(database); + return { + ...real, + insertPendingCleanupLease: () => + Promise.reject(new Error("pending-cleanup write failed; database down")), + }; + }); + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockResolvedValue(undefined as never); + try { + // Set the retry backoff to zero, so the failed write retries never slow the + // test. + const runtime = environmentRuntimeService(db, { pendingCleanupWriteBackoffMs: 0 }); + const rejection = await runtime + .acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }) + .then( + () => { + throw new Error("acquireRunLease resolved but must reject"); + }, + (error: unknown) => error, + ); + + // The rejection is the original company mismatch, not an orphan-cleanup + // write error, because the teardown removed the orphan. + expect(rejection).not.toBeInstanceOf(SandboxOrphanCleanupWriteError); + expect(rejection).toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } }); + expect(destroySpy).toHaveBeenCalledTimes(1); + + // No orphan remains and no durable row was needed, so the lease table is + // empty for the environment. + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(0); + + // The buffer holds no orphan, so a flush recovers nothing. + const flushed = await runtime.flushDeferredOrphanCleanups(); + expect(flushed).toEqual({ recovered: 0, pending: 0 }); + } finally { + destroySpy.mockRestore(); + factorySpy.mockRestore(); + } + }); + + it("records a durable pending-cleanup lease when the plugin teardown fails after a foreign-company rejection", async () => { + const pluginId = randomUUID(); + const { companyId, environment: baseEnvironment, runId } = await seedEnvironment(); + const pluginConfig = { provider: "fake-plugin", image: "fake:test", reuseLease: false }; + const environment = { + ...baseEnvironment, + name: "Foreign-bound Plugin Sandbox Teardown Fail", + driver: "sandbox", + config: pluginConfig, + }; + await environmentService(db).update(environment.id, { + driver: "sandbox", + name: environment.name, + config: pluginConfig, + }); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclip.fake-plugin-sandbox-provider", + packageName: "@paperclipai/plugin-fake-sandbox", + version: "1.0.0", + apiVersion: 1, + categories: ["automation"], + manifestJson: { + id: "paperclip.fake-plugin-sandbox-provider", + apiVersion: 1, + version: "1.0.0", + displayName: "Fake Plugin Sandbox Provider", + description: "Test fake plugin provider", + author: "Paperclip", + categories: ["automation"], + capabilities: ["environment.drivers.register"], + entrypoints: { worker: "dist/worker.js" }, + environmentDrivers: [ + { + driverKey: "fake-plugin", + kind: "sandbox_provider", + displayName: "Fake Plugin", + configSchema: { type: "object" }, + }, + ], + }, + status: "ready", + installOrder: 1, + updatedAt: new Date(), + } as any); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Plugin", + issuePrefix: "OTD", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + const workerManager = { + isRunning: vi.fn((id: string) => id === pluginId), + getWorker: vi.fn(() => ({ supportedMethods: ["environmentDestroyLease"] })), + call: vi.fn(async (_pluginId: string, method: string) => { + if (method === "environmentAcquireLease") { + return { + providerLeaseId: "plugin-lease-2", + metadata: { provider: "fake-plugin", image: "fake:test", reuseLease: false }, + }; + } + if (method === "environmentDestroyLease") { + throw new Error("destroy failed"); + } + throw new Error(`Unexpected plugin method: ${method}`); + }), + } as unknown as PluginWorkerManager; + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + + await expect( + runtimeWithPlugin.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }), + ).rejects.toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } }); + + // The failed plugin teardown left a durable pending-cleanup row that carries + // the provider lease id for a later sweep. + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("pending_cleanup"); + expect(rows[0]?.cleanupStatus).toBe("failed"); + expect(rows[0]?.providerLeaseId).toBe("plugin-lease-2"); + + await environmentService(db).update(environment.id, { driver: "local", config: {} }); + }); + + it("throws a SandboxOrphanCleanupWriteError when the built-in durable pending-cleanup write also fails", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox Cleanup Write Fail", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Cleanup Write", + issuePrefix: "OCW", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + // The remote teardown fails, so the acquire tries to record a durable + // pending-cleanup row instead. + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValue(new Error("teardown failed")); + // Force the durable pending-cleanup write to fail on every attempt. The + // write retries a few times, so reject the insert each time and let the + // primary company-bound acquire reject for real. + const failingInsert = vi + .fn["insertPendingCleanupLease"]>, Promise>() + .mockRejectedValue(new Error("pending-cleanup write failed")); + const realEnvironmentService = environmentsModule.environmentService; + const factorySpy = vi + .spyOn(environmentsModule, "environmentService") + .mockImplementation((database: Parameters[0]) => { + const real = realEnvironmentService(database); + return { + ...real, + insertPendingCleanupLease: failingInsert, + }; + }); + // The durable database write fails, so the only durable handle left is the + // error log. Capture it to prove the leaked sandbox stays discoverable. + const logSpy = vi.spyOn(logger, "error").mockImplementation(() => logger); + try { + // Set the retry backoff to zero, so the retries never slow the test. + const runtimeWithFailingCleanup = environmentRuntimeService(db, { + pendingCleanupWriteBackoffMs: 0, + }); + const rejection = await runtimeWithFailingCleanup + .acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }) + .then( + () => { + throw new Error("acquireRunLease resolved but must reject"); + }, + (error: unknown) => error, + ); + + // The failed durable write is not swallowed: the acquire throws a + // SandboxOrphanCleanupWriteError that keeps the original 403 rejection as + // its cause. + expect(rejection).toBeInstanceOf(SandboxOrphanCleanupWriteError); + expect(rejection).toMatchObject({ provider: "fake" }); + expect((rejection as SandboxOrphanCleanupWriteError).cause).toMatchObject({ + status: 403, + details: { code: "environment_company_mismatch" }, + }); + expect(destroySpy).toHaveBeenCalledTimes(1); + // The write retried the durable insert before it gave up, so more than one + // attempt ran. + expect(failingInsert.mock.calls.length).toBeGreaterThan(1); + // The durable write failed before it created a row, so no lease row exists. + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(0); + // The durable log preserves the provider identifiers, so an operator keeps + // a handle to find and tear the leaked sandbox down by hand. + const cleanupLog = logSpy.mock.calls.find( + ([fields]) => + (fields as { errorKind?: string } | undefined)?.errorKind === + "sandbox_orphan_cleanup_write_failed", + ); + expect(cleanupLog).toBeDefined(); + expect(cleanupLog?.[0]).toMatchObject({ + provider: "fake", + companyId, + environmentId: environment.id, + }); + // The log never carries the caught exception, so no credential leaks. + expect(cleanupLog?.[0]).not.toHaveProperty("cause"); + expect(cleanupLog?.[0]).not.toHaveProperty("cleanupWriteError"); + } finally { + logSpy.mockRestore(); + factorySpy.mockRestore(); + destroySpy.mockRestore(); + } + }); + + it("recovers cleanup state when a retry of the durable pending-cleanup write succeeds", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox Cleanup Write Retry", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Cleanup Retry", + issuePrefix: "OCR", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + // The remote teardown fails, so the acquire tries to record a durable + // pending-cleanup row instead. + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValue(new Error("teardown failed")); + // The first durable write attempt fails, so the acquire retries. The second + // attempt delegates to the real insert, so a durable pending_cleanup row + // lands and a later sweep can find the orphan. + let insertAttempts = 0; + const realEnvironmentService = environmentsModule.environmentService; + const factorySpy = vi + .spyOn(environmentsModule, "environmentService") + .mockImplementation((database: Parameters[0]) => { + const real = realEnvironmentService(database); + return { + ...real, + insertPendingCleanupLease: ( + input: Parameters[0], + ) => { + insertAttempts += 1; + if (insertAttempts === 1) { + return Promise.reject(new Error("pending-cleanup write failed once")); + } + return real.insertPendingCleanupLease(input); + }, + }; + }); + try { + // Set the retry backoff to zero, so the retry never slows the test. + const runtimeWithRetry = environmentRuntimeService(db, { + pendingCleanupWriteBackoffMs: 0, + }); + const rejection = await runtimeWithRetry + .acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }) + .then( + () => { + throw new Error("acquireRunLease resolved but must reject"); + }, + (error: unknown) => error, + ); + + // The retry landed the durable row, so the acquire rejects with the + // original company-mismatch rejection, not a SandboxOrphanCleanupWriteError. + expect(rejection).not.toBeInstanceOf(SandboxOrphanCleanupWriteError); + expect(rejection).toMatchObject({ + status: 403, + details: { code: "environment_company_mismatch" }, + }); + expect(destroySpy).toHaveBeenCalledTimes(1); + // The first attempt failed and the second succeeded, so the write ran twice. + expect(insertAttempts).toBe(2); + // A durable pending_cleanup lease row now tracks the orphan, so a sweep can + // find and release the leaked sandbox. + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("pending_cleanup"); + expect(rows[0]?.cleanupStatus).toBe("failed"); + expect(rows[0]?.failureReason).toBe("acquire_rejected_teardown_failed"); + } finally { + factorySpy.mockRestore(); + destroySpy.mockRestore(); + } + }); + + it("buffers the orphan in-process and a later sweep flush lands the durable row after the database recovers", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox Cleanup Buffer Flush", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Cleanup Buffer", + issuePrefix: "OCB", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + // The remote teardown fails, so the acquire tries to record a durable + // pending-cleanup row instead. + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValue(new Error("teardown failed")); + // The database is down, so every synchronous pending-cleanup write rejects. + // The flag flips to false when the database recovers, so a later flush lands + // the durable row. + let databaseDown = true; + const realEnvironmentService = environmentsModule.environmentService; + const factorySpy = vi + .spyOn(environmentsModule, "environmentService") + .mockImplementation((database: Parameters[0]) => { + const real = realEnvironmentService(database); + return { + ...real, + insertPendingCleanupLease: ( + input: Parameters[0], + ) => { + if (databaseDown) { + return Promise.reject(new Error("pending-cleanup write failed; database down")); + } + return real.insertPendingCleanupLease(input); + }, + }; + }); + const logSpy = vi.spyOn(logger, "error").mockImplementation(() => logger); + try { + // Set the retry backoff to zero, so the retries never slow the test. + const runtime = environmentRuntimeService(db, { pendingCleanupWriteBackoffMs: 0 }); + const rejection = await runtime + .acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }) + .then( + () => { + throw new Error("acquireRunLease resolved but must reject"); + }, + (error: unknown) => error, + ); + + // The synchronous write failed on every attempt, so the acquire still + // throws the orphan-cleanup write error that keeps the original rejection. + expect(rejection).toBeInstanceOf(SandboxOrphanCleanupWriteError); + // The database was down, so no durable row exists yet. + let rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(0); + // The acquire buffered the orphan in-process, so the log records the buffer. + const bufferedLog = logSpy.mock.calls.find( + ([fields]) => + (fields as { errorKind?: string } | undefined)?.errorKind === + "sandbox_orphan_cleanup_write_failed", + ); + expect(bufferedLog?.[0]).toMatchObject({ buffered: true }); + + // The database recovers, so the next cleanup-sweep flush lands the row. + databaseDown = false; + const flushed = await runtime.flushDeferredOrphanCleanups(); + expect(flushed).toEqual({ recovered: 1, pending: 0 }); + + // A durable pending_cleanup row now tracks the orphan, so a sweep finds and + // releases the leaked sandbox. + rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("pending_cleanup"); + expect(rows[0]?.cleanupStatus).toBe("failed"); + expect(rows[0]?.failureReason).toBe("acquire_rejected_teardown_failed"); + expect(rows[0]?.providerLeaseId).toBeTruthy(); + + // The buffer is empty now, so a second flush inserts nothing. + const second = await runtime.flushDeferredOrphanCleanups(); + expect(second).toEqual({ recovered: 0, pending: 0 }); + const rowsAfter = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rowsAfter).toHaveLength(1); + } finally { + logSpy.mockRestore(); + factorySpy.mockRestore(); + destroySpy.mockRestore(); + } + }); + + it("persists the orphan to the durable spool so a restart recovers and lands the durable row", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox Cleanup Spool Restart", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Cleanup Spool Restart", + issuePrefix: "OCS", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + // The remote teardown fails, so the acquire tries to record a durable + // pending-cleanup row instead. + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValue(new Error("teardown failed")); + // The database is down, so every synchronous pending-cleanup write rejects. + // The flag flips to false to simulate the database recovering after a + // restart. + let databaseDown = true; + const realEnvironmentService = environmentsModule.environmentService; + const factorySpy = vi + .spyOn(environmentsModule, "environmentService") + .mockImplementation((database: Parameters[0]) => { + const real = realEnvironmentService(database); + return { + ...real, + insertPendingCleanupLease: ( + input: Parameters[0], + ) => { + if (databaseDown) { + return Promise.reject(new Error("pending-cleanup write failed; database down")); + } + return real.insertPendingCleanupLease(input); + }, + }; + }); + const logSpy = vi.spyOn(logger, "error").mockImplementation(() => logger); + const spoolDir = process.env.SANDBOX_ORPHAN_CLEANUP_SPOOL_DIR!; + try { + // The first process acquires, fails every synchronous write, and persists + // the orphan to the durable spool. The retry backoff is zero, so the test + // never waits. + const firstProcessRuntime = environmentRuntimeService(db, { pendingCleanupWriteBackoffMs: 0 }); + const rejection = await firstProcessRuntime + .acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }) + .then( + () => { + throw new Error("acquireRunLease resolved but must reject"); + }, + (error: unknown) => error, + ); + expect(rejection).toBeInstanceOf(SandboxOrphanCleanupWriteError); + + // The database was down, so no durable row exists yet. + let rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(0); + + // The acquire persisted the orphan to the durable spool, so a file waits on + // the disk and the log records the durable persist. + const spoolFiles = (await readdir(spoolDir)).filter((name) => name.endsWith(".json")); + expect(spoolFiles).toHaveLength(1); + const persistedLog = logSpy.mock.calls.find( + ([fields]) => + (fields as { errorKind?: string } | undefined)?.errorKind === + "sandbox_orphan_cleanup_write_failed", + ); + expect(persistedLog?.[0]).toMatchObject({ persisted: true }); + + // The process restarts. A fresh runtime keeps no in-process buffer, so only + // the durable spool carries the orphan. The database recovers, so the first + // cleanup-sweep flush loads the spool and lands the durable row. + databaseDown = false; + const restartedRuntime = environmentRuntimeService(db, { pendingCleanupWriteBackoffMs: 0 }); + const flushed = await restartedRuntime.flushDeferredOrphanCleanups(); + expect(flushed).toEqual({ recovered: 1, pending: 0 }); + + rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("pending_cleanup"); + expect(rows[0]?.failureReason).toBe("acquire_rejected_teardown_failed"); + expect(rows[0]?.providerLeaseId).toBeTruthy(); + + // The flush removed the spooled copy after the row landed, so a second + // restart never re-inserts a duplicate. + const spoolFilesAfter = (await readdir(spoolDir)).filter((name) => name.endsWith(".json")); + expect(spoolFilesAfter).toHaveLength(0); + const secondRestartRuntime = environmentRuntimeService(db, { pendingCleanupWriteBackoffMs: 0 }); + const secondFlush = await secondRestartRuntime.flushDeferredOrphanCleanups(); + expect(secondFlush).toEqual({ recovered: 0, pending: 0 }); + const rowsAfter = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rowsAfter).toHaveLength(1); + } finally { + logSpy.mockRestore(); + factorySpy.mockRestore(); + destroySpy.mockRestore(); + } + }); + + it("buffers the orphan in-process when the durable spool write also fails", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox Cleanup Spool Unwritable", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Cleanup Spool Unwritable", + issuePrefix: "OCU", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValue(new Error("teardown failed")); + let databaseDown = true; + const realEnvironmentService = environmentsModule.environmentService; + const factorySpy = vi + .spyOn(environmentsModule, "environmentService") + .mockImplementation((database: Parameters[0]) => { + const real = realEnvironmentService(database); + return { + ...real, + insertPendingCleanupLease: ( + input: Parameters[0], + ) => { + if (databaseDown) { + return Promise.reject(new Error("pending-cleanup write failed; database down")); + } + return real.insertPendingCleanupLease(input); + }, + }; + }); + const logSpy = vi.spyOn(logger, "error").mockImplementation(() => logger); + try { + // Point the spool at an unwritable path, so the durable spool write fails + // like a full or read-only disk. The acquire must fall back to the + // in-process buffer and never lose the orphan silently. + const unwritableSpool = { + append: () => Promise.resolve(false), + load: () => Promise.resolve([]), + remove: () => Promise.resolve(), + }; + const runtime = environmentRuntimeService(db, { + pendingCleanupWriteBackoffMs: 0, + orphanCleanupSpool: unwritableSpool, + }); + await runtime + .acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }) + .then( + () => { + throw new Error("acquireRunLease resolved but must reject"); + }, + () => undefined, + ); + + // The spool write failed, so the log records the in-process-only fallback. + const fallbackLog = logSpy.mock.calls.find( + ([fields]) => + (fields as { errorKind?: string } | undefined)?.errorKind === + "sandbox_orphan_cleanup_write_failed", + ); + expect(fallbackLog?.[0]).toMatchObject({ persisted: false, buffered: true }); + + // The same runtime still keeps the orphan in-process, so a flush after the + // database recovers lands the durable row. + databaseDown = false; + const flushed = await runtime.flushDeferredOrphanCleanups(); + expect(flushed).toEqual({ recovered: 1, pending: 0 }); + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("pending_cleanup"); + } finally { + logSpy.mockRestore(); + factorySpy.mockRestore(); + destroySpy.mockRestore(); + } + }); + + it("keeps the buffered orphan when the flush write still fails, then recovers on a later flush", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Foreign-bound Fake Sandbox Cleanup Buffer Requeue", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Cleanup Requeue", + issuePrefix: "OCQ", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValue(new Error("teardown failed")); + let databaseDown = true; + const realEnvironmentService = environmentsModule.environmentService; + const factorySpy = vi + .spyOn(environmentsModule, "environmentService") + .mockImplementation((database: Parameters[0]) => { + const real = realEnvironmentService(database); + return { + ...real, + insertPendingCleanupLease: ( + input: Parameters[0], + ) => { + if (databaseDown) { + return Promise.reject(new Error("pending-cleanup write failed; database down")); + } + return real.insertPendingCleanupLease(input); + }, + }; + }); + const errorLogSpy = vi.spyOn(logger, "error").mockImplementation(() => logger); + const warnLogSpy = vi.spyOn(logger, "warn").mockImplementation(() => logger); + try { + const runtime = environmentRuntimeService(db, { pendingCleanupWriteBackoffMs: 0 }); + await runtime + .acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }) + .then( + () => { + throw new Error("acquireRunLease resolved but must reject"); + }, + () => undefined, + ); + + // The database is still down, so the flush re-queues the orphan instead of + // losing it. The buffer keeps the record for a later tick. + const firstFlush = await runtime.flushDeferredOrphanCleanups(); + expect(firstFlush).toEqual({ recovered: 0, pending: 1 }); + const rowsWhileDown = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rowsWhileDown).toHaveLength(0); + // The flush logs the re-queue and never carries the caught write exception. + // The sync-retry path also warns with the same error kind, so match on the + // `requeued` field that only the flush warn carries. + const requeueLog = warnLogSpy.mock.calls.find( + ([fields]) => (fields as { requeued?: boolean } | undefined)?.requeued === true, + ); + expect(requeueLog).toBeDefined(); + expect(requeueLog?.[0]).toMatchObject({ + errorKind: "sandbox_orphan_cleanup_write_failed", + requeued: true, + }); + expect(requeueLog?.[0]).not.toHaveProperty("cause"); + expect(requeueLog?.[0]).not.toHaveProperty("cleanupWriteError"); + + // The database recovers, so the next flush lands the durable row exactly + // once from the still-buffered record. + databaseDown = false; + const secondFlush = await runtime.flushDeferredOrphanCleanups(); + expect(secondFlush).toEqual({ recovered: 1, pending: 0 }); + const rowsAfter = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rowsAfter).toHaveLength(1); + expect(rowsAfter[0]?.status).toBe("pending_cleanup"); + } finally { + warnLogSpy.mockRestore(); + errorLogSpy.mockRestore(); + factorySpy.mockRestore(); + destroySpy.mockRestore(); + } + }); + + it("throws a SandboxOrphanCleanupWriteError when the plugin durable pending-cleanup write also fails", async () => { + const pluginId = randomUUID(); + const { companyId, environment: baseEnvironment, runId } = await seedEnvironment(); + const pluginConfig = { provider: "fake-plugin", image: "fake:test", reuseLease: false }; + const environment = { + ...baseEnvironment, + name: "Foreign-bound Plugin Sandbox Cleanup Write Fail", + driver: "sandbox", + config: pluginConfig, + }; + await environmentService(db).update(environment.id, { + driver: "sandbox", + name: environment.name, + config: pluginConfig, + }); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclip.fake-plugin-sandbox-provider", + packageName: "@paperclipai/plugin-fake-sandbox", + version: "1.0.0", + apiVersion: 1, + categories: ["automation"], + manifestJson: { + id: "paperclip.fake-plugin-sandbox-provider", + apiVersion: 1, + version: "1.0.0", + displayName: "Fake Plugin Sandbox Provider", + description: "Test fake plugin provider", + author: "Paperclip", + categories: ["automation"], + capabilities: ["environment.drivers.register"], + entrypoints: { worker: "dist/worker.js" }, + environmentDrivers: [ + { + driverKey: "fake-plugin", + kind: "sandbox_provider", + displayName: "Fake Plugin", + configSchema: { type: "object" }, + }, + ], + }, + status: "ready", + installOrder: 1, + updatedAt: new Date(), + } as any); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Plugin Cleanup Write", + issuePrefix: "OPW", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + const workerManager = { + isRunning: vi.fn((id: string) => id === pluginId), + getWorker: vi.fn(() => ({ supportedMethods: ["environmentDestroyLease"] })), + call: vi.fn(async (_pluginId: string, method: string) => { + if (method === "environmentAcquireLease") { + return { + providerLeaseId: "plugin-lease-write-fail", + metadata: { provider: "fake-plugin", image: "fake:test", reuseLease: false }, + }; + } + if (method === "environmentDestroyLease") { + throw new Error("destroy failed"); + } + throw new Error(`Unexpected plugin method: ${method}`); + }), + } as unknown as PluginWorkerManager; + + const realEnvironmentService = environmentsModule.environmentService; + const factorySpy = vi + .spyOn(environmentsModule, "environmentService") + .mockImplementation((database: Parameters[0]) => { + const real = realEnvironmentService(database); + return { + ...real, + insertPendingCleanupLease: () => + Promise.reject(new Error("pending-cleanup write failed")), + }; + }); + try { + const runtimeWithPlugin = environmentRuntimeService(db, { + pluginWorkerManager: workerManager, + pendingCleanupWriteBackoffMs: 0, + }); + const rejection = await runtimeWithPlugin + .acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }) + .then( + () => { + throw new Error("acquireRunLease resolved but must reject"); + }, + (error: unknown) => error, + ); + + expect(rejection).toBeInstanceOf(SandboxOrphanCleanupWriteError); + expect(rejection).toMatchObject({ provider: "fake-plugin", providerLeaseId: "plugin-lease-write-fail" }); + expect((rejection as SandboxOrphanCleanupWriteError).cause).toMatchObject({ + status: 403, + details: { code: "environment_company_mismatch" }, + }); + // The durable write failed before it created a row, so no lease row exists. + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(0); + } finally { + factorySpy.mockRestore(); + } + + await environmentService(db).update(environment.id, { driver: "local", config: {} }); + }); + + it("records the orphan directly in the pending_cleanup state with one atomic insert", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Atomic Pending Cleanup Insert", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + + const lease = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + executionWorkspaceId: null, + issueId: null, + heartbeatRunId: runId, + provider: "fake", + providerLeaseId: "sandbox://fake/atomic-orphan", + metadata: { provider: "fake", reuseLease: false }, + failureReason: "acquire_rejected_teardown_failed", + }); + + // The returned lease is already in the terminal recovery state. It never + // passes through the `active` state, so a crash cannot strand the orphan. + expect(lease.status).toBe("pending_cleanup"); + expect(lease.cleanupStatus).toBe("failed"); + expect(lease.leasePolicy).toBe("ephemeral"); + expect(lease.failureReason).toBe("acquire_rejected_teardown_failed"); + expect(lease.providerLeaseId).toBe("sandbox://fake/atomic-orphan"); + expect(lease.releasedAt).not.toBeNull(); + + // One insert created exactly one row, and the sweep reads that row directly. + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("pending_cleanup"); + expect(rows[0]?.cleanupStatus).toBe("failed"); + }); + + it("tears the recorded built-in provider lease down for a pending-cleanup orphan", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Cleanup Retry Built-in", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Retry Built-in", + issuePrefix: "OTE", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + // The first teardown fails, so the acquire records a pending-cleanup orphan. + // The sweep then retries the teardown, which succeeds this time. + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValueOnce(new Error("teardown failed")) + .mockResolvedValue(undefined); + try { + await expect( + runtime.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }), + ).rejects.toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } }); + + const orphan = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)) + .then((r) => r[0]!); + expect(orphan.status).toBe("pending_cleanup"); + const providerLeaseId = orphan.providerLeaseId; + + // The teardown accepts a null environment and reads the recorded provider + // lease, so a delete or a provider change never strands it. + const lease = await environmentService(db).getLeaseById(orphan.id); + await runtime.retryPendingSandboxTeardown({ environment: null, lease: lease! }); + // The retry called the provider teardown a second time with the orphan + // provider lease id. + expect(destroySpy).toHaveBeenCalledTimes(2); + expect(destroySpy).toHaveBeenLastCalledWith(expect.objectContaining({ providerLeaseId })); + } finally { + destroySpy.mockRestore(); + } + }); + + it("throws and keeps the pending-cleanup orphan when the teardown retry fails", async () => { + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Cleanup Retry Scope", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Retry Scope", + issuePrefix: "OTF", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValue(new Error("teardown failed")); + try { + await expect( + runtime.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }), + ).rejects.toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } }); + + const orphan = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)) + .then((r) => r[0]!); + expect(orphan.status).toBe("pending_cleanup"); + + // The teardown retries, but the provider destroy still fails, so the + // teardown throws. The caller keeps the orphan pending for a later sweep. + const lease = await environmentService(db).getLeaseById(orphan.id); + await expect( + runtime.retryPendingSandboxTeardown({ environment: null, lease: lease! }), + ).rejects.toThrow(); + + const still = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.id, orphan.id)) + .then((r) => r[0]!); + expect(still.status).toBe("pending_cleanup"); + expect(still.cleanupStatus).toBe("failed"); + } finally { + destroySpy.mockRestore(); + } + }); + + it("tears the recorded provider down after the environment provider changes", async () => { + // Ordering: an acquire provisions a sandbox with provider A, the lease + // insert rejects a foreign-company binding, and the compensating teardown + // fails, so the acquire records an orphan for provider A. A provider change + // then re-points the environment before the sweep runs. The sweep must tear + // the orphan down from the recorded provider metadata, not the current + // environment provider, so the change cannot strand the teardown. + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Provider Change Vs Orphan", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Provider Change", + issuePrefix: "OTP", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockRejectedValueOnce(new Error("teardown failed")) + .mockResolvedValue(undefined); + try { + await expect( + runtime.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }), + ).rejects.toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } }); + + const orphan = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)) + .then((r) => r[0]!); + expect(orphan.status).toBe("pending_cleanup"); + expect(orphan.provider).toBe("fake"); + const providerLeaseId = orphan.providerLeaseId; + + // The provider-target mutation re-points the environment to a different + // driver. The orphan teardown must not depend on this current config. + await environmentService(db).update(environment.id, { driver: "local", config: {} }); + + // The teardown tears the recorded provider lease down, so it never reads + // the re-pointed environment provider. + const lease = await environmentService(db).getLeaseById(orphan.id); + await runtime.retryPendingSandboxTeardown({ environment: null, lease: lease! }); + expect(destroySpy).toHaveBeenLastCalledWith(expect.objectContaining({ providerLeaseId })); + } finally { + destroySpy.mockRestore(); + } + }); + + it("reports the plugin cleanup worker not ready while its worker is down, then ready after it recovers", async () => { + // A pending_cleanup orphan targets a plugin-backed provider. The sweep must + // not consume a finite retry attempt while the plugin worker is briefly down. + // So the readiness probe reports "not ready" for a down worker and "ready" + // after the worker recovers. A built-in provider has no worker, so it is + // always ready. + const pluginId = randomUUID(); + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Cleanup Worker Readiness", + config: { provider: "fake-plugin-ready", image: "fake:test", reuseLease: false }, + }); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclip.fake-plugin-ready-sandbox-provider", + packageName: "@paperclipai/plugin-fake-ready-sandbox", + version: "1.0.0", + apiVersion: 1, + categories: ["automation"], + manifestJson: { + id: "paperclip.fake-plugin-ready-sandbox-provider", + apiVersion: 1, + version: "1.0.0", + displayName: "Fake Plugin Ready Sandbox Provider", + description: "Test fake plugin provider readiness", + author: "Paperclip", + categories: ["automation"], + capabilities: ["environment.drivers.register"], + entrypoints: { worker: "dist/worker.js" }, + environmentDrivers: [ + { + driverKey: "fake-plugin-ready", + kind: "sandbox_provider", + displayName: "Fake Plugin Ready", + configSchema: { type: "object" }, + }, + ], + }, + status: "ready", + installOrder: 1, + updatedAt: new Date(), + } as any); + + // The worker is down at first, then recovers on the second probe. + let workerRunning = false; + const workerManager = { + isRunning: vi.fn((id: string) => id === pluginId && workerRunning), + call: vi.fn(async () => { + throw new Error("call must not run during a readiness probe"); + }), + } as unknown as PluginWorkerManager; + + const orphan = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + executionWorkspaceId: null, + issueId: null, + heartbeatRunId: runId, + provider: "fake-plugin-ready", + providerLeaseId: "plugin-lease-readiness", + metadata: { provider: "fake-plugin-ready", driver: "sandbox", reuseLease: false }, + failureReason: "acquire_rejected_teardown_failed", + }); + const lease = (await environmentService(db).getLeaseById(orphan.id))!; + + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + + // A down plugin worker is the transient restart window, so the probe reports + // not ready. The sweep skips the lease without a claim, so no attempt burns. + await expect( + runtimeWithPlugin.isPendingCleanupWorkerReady({ environment, lease }), + ).resolves.toBe(false); + // The probe never calls the worker; it only checks the live worker state. + expect((workerManager.call as ReturnType)).not.toHaveBeenCalled(); + + // The worker recovers, so the probe now reports ready and the sweep proceeds. + workerRunning = true; + await expect( + runtimeWithPlugin.isPendingCleanupWorkerReady({ environment, lease }), + ).resolves.toBe(true); + + // A built-in provider has no plugin worker, so its orphan is always ready. + const builtinOrphan = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + executionWorkspaceId: null, + issueId: null, + heartbeatRunId: runId, + provider: "fake", + providerLeaseId: "builtin-lease-readiness", + metadata: { provider: "fake", driver: "sandbox", reuseLease: false }, + failureReason: "acquire_rejected_teardown_failed", + }); + const builtinLease = (await environmentService(db).getLeaseById(builtinOrphan.id))!; + await expect( + runtimeWithPlugin.isPendingCleanupWorkerReady({ environment, lease: builtinLease }), + ).resolves.toBe(true); + }); + + it("reports the plugin cleanup provider not ready while the plugin is not ready, then ready after it recovers", async () => { + // A pending_cleanup orphan targets a plugin-backed provider. A plugin reload + // or a plugin reinstall moves the plugin through a "not ready" status. The + // sweep must not consume a finite retry attempt in that transient window. So + // the readiness probe reports "not ready" while the plugin status is not + // "ready", even when its worker runs, and "ready" after the plugin recovers. + const pluginId = randomUUID(); + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Cleanup Plugin Readiness", + config: { provider: "fake-plugin-reload", image: "fake:test", reuseLease: false }, + }); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclip.fake-plugin-reload-sandbox-provider", + packageName: "@paperclipai/plugin-fake-reload-sandbox", + version: "1.0.0", + apiVersion: 1, + categories: ["automation"], + manifestJson: { + id: "paperclip.fake-plugin-reload-sandbox-provider", + apiVersion: 1, + version: "1.0.0", + displayName: "Fake Plugin Reload Sandbox Provider", + description: "Test fake plugin provider reload readiness", + author: "Paperclip", + categories: ["automation"], + capabilities: ["environment.drivers.register"], + entrypoints: { worker: "dist/worker.js" }, + environmentDrivers: [ + { + driverKey: "fake-plugin-reload", + kind: "sandbox_provider", + displayName: "Fake Plugin Reload", + configSchema: { type: "object" }, + }, + ], + }, + // The plugin starts in a not-ready status, as during a reload. + status: "installing", + installOrder: 1, + updatedAt: new Date(), + } as any); + + // The worker runs the whole time, so the probe result depends on the plugin + // status alone, not on a down worker. + const workerManager = { + isRunning: vi.fn((id: string) => id === pluginId), + call: vi.fn(async () => { + throw new Error("call must not run during a readiness probe"); + }), + } as unknown as PluginWorkerManager; + + const orphan = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + executionWorkspaceId: null, + issueId: null, + heartbeatRunId: runId, + provider: "fake-plugin-reload", + providerLeaseId: "plugin-lease-reload", + metadata: { provider: "fake-plugin-reload", driver: "sandbox", reuseLease: false }, + failureReason: "acquire_rejected_teardown_failed", + }); + const lease = (await environmentService(db).getLeaseById(orphan.id))!; + + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + + // A not-ready plugin is the transient reload window, so the probe reports + // not ready. The sweep skips the lease without a claim, so no attempt burns. + await expect( + runtimeWithPlugin.isPendingCleanupWorkerReady({ environment, lease }), + ).resolves.toBe(false); + expect((workerManager.call as ReturnType)).not.toHaveBeenCalled(); + + // The plugin becomes ready, so the probe now reports ready and the sweep + // proceeds. + await db.update(plugins).set({ status: "ready" }).where(eq(plugins.id, pluginId)); + await expect( + runtimeWithPlugin.isPendingCleanupWorkerReady({ environment, lease }), + ).resolves.toBe(true); + }); + + it("reports the plugin cleanup provider not ready while the plugin is missing, then ready after it returns", async () => { + // A pending_cleanup orphan targets a plugin-backed provider whose plugin row + // is gone. A plugin reinstall removes and re-adds the plugin row, so the row + // is missing for a short window. The sweep must not consume a finite retry + // attempt in that window. So the readiness probe reports "not ready" while no + // plugin row resolves the provider, and "ready" after the plugin returns. + const pluginId = randomUUID(); + const { companyId, environment, runId } = await seedEnvironment({ + driver: "sandbox", + name: "Cleanup Plugin Missing", + config: { provider: "fake-plugin-missing", image: "fake:test", reuseLease: false }, + }); + + // The worker manager reports the plugin worker as running, so the probe + // result depends on the missing plugin row alone, not on a down worker. + const workerManager = { + isRunning: vi.fn((id: string) => id === pluginId), + call: vi.fn(async () => { + throw new Error("call must not run during a readiness probe"); + }), + } as unknown as PluginWorkerManager; + + const orphan = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + executionWorkspaceId: null, + issueId: null, + heartbeatRunId: runId, + provider: "fake-plugin-missing", + providerLeaseId: "plugin-lease-missing", + metadata: { provider: "fake-plugin-missing", driver: "sandbox", reuseLease: false }, + failureReason: "acquire_rejected_teardown_failed", + }); + const lease = (await environmentService(db).getLeaseById(orphan.id))!; + + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + + // No plugin row resolves the provider, so the probe reports not ready. The + // sweep skips the lease without a claim, so no attempt burns while the plugin + // reinstall is in flight. + await expect( + runtimeWithPlugin.isPendingCleanupWorkerReady({ environment, lease }), + ).resolves.toBe(false); + expect((workerManager.call as ReturnType)).not.toHaveBeenCalled(); + + // The plugin reinstall completes, so a ready plugin row now resolves the + // provider and the probe reports ready. + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclip.fake-plugin-missing-sandbox-provider", + packageName: "@paperclipai/plugin-fake-missing-sandbox", + version: "1.0.0", + apiVersion: 1, + categories: ["automation"], + manifestJson: { + id: "paperclip.fake-plugin-missing-sandbox-provider", + apiVersion: 1, + version: "1.0.0", + displayName: "Fake Plugin Missing Sandbox Provider", + description: "Test fake plugin provider missing readiness", + author: "Paperclip", + categories: ["automation"], + capabilities: ["environment.drivers.register"], + entrypoints: { worker: "dist/worker.js" }, + environmentDrivers: [ + { + driverKey: "fake-plugin-missing", + kind: "sandbox_provider", + displayName: "Fake Plugin Missing", + configSchema: { type: "object" }, + }, + ], + }, + status: "ready", + installOrder: 1, + updatedAt: new Date(), + } as any); + await expect( + runtimeWithPlugin.isPendingCleanupWorkerReady({ environment, lease }), + ).resolves.toBe(true); + }); + + it("records an orphan with a null reference when a delete already removed the environment", async () => { + // Ordering: a delete removes the environment before the acquire records the + // orphan. The environment foreign key must not fail the insert. The record + // persists with a null reference and its immutable provider metadata, so a + // later sweep still tears the orphan down. + const { companyId, environment } = await seedEnvironment({ + driver: "sandbox", + name: "Delete Before Orphan Record", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + + // Remove the environment row first, so the record happens after the delete. + await environmentService(db).remove(environment.id); + + const orphan = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + provider: "fake", + providerLeaseId: "sandbox://fake/delete-before-record", + metadata: { provider: "fake", image: "ubuntu:24.04", driver: "sandbox" }, + failureReason: "acquire_rejected_teardown_failed", + }); + // The insert kept no environment reference, so the delete did not fail it. + expect(orphan.environmentId).toBeNull(); + expect(orphan.status).toBe("pending_cleanup"); + + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockResolvedValue(undefined); + try { + // The recorded metadata drives the teardown, so a null environment tears + // the orphan down. + const lease = await environmentService(db).getLeaseById(orphan.id); + await runtime.retryPendingSandboxTeardown({ environment: null, lease: lease! }); + expect(destroySpy).toHaveBeenCalledWith( + expect.objectContaining({ providerLeaseId: "sandbox://fake/delete-before-record" }), + ); + } finally { + destroySpy.mockRestore(); + } + }); + + it("keeps the orphan and tears it down after the environment is deleted", async () => { + // Ordering: an orphan is recorded, then the environment is deleted. The + // foreign key sets the reference null instead of cascading the row away, so + // the orphan survives. The sweep tears it down from the recorded metadata. + const { companyId, environment } = await seedEnvironment({ + driver: "sandbox", + name: "Delete After Orphan Record", + config: { provider: "fake", image: "ubuntu:24.04", reuseLease: false }, + }); + + const orphan = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + provider: "fake", + providerLeaseId: "sandbox://fake/delete-after-record", + metadata: { provider: "fake", image: "ubuntu:24.04", driver: "sandbox" }, + failureReason: "acquire_rejected_teardown_failed", + }); + expect(orphan.environmentId).toBe(environment.id); + + // Delete the environment row directly. The foreign key sets the lease + // reference null, so the orphan survives instead of being cascaded away. + await environmentService(db).remove(environment.id); + const survived = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.id, orphan.id)) + .then((r) => r[0]!); + expect(survived.environmentId).toBeNull(); + expect(survived.status).toBe("pending_cleanup"); + + const destroySpy = vi + .spyOn(sandboxProviderRuntime, "destroySandboxProviderLease") + .mockResolvedValue(undefined); + try { + // The recorded metadata drives the teardown, so a null environment tears + // the orphan down after the delete set the reference null. + const lease = await environmentService(db).getLeaseById(orphan.id); + await runtime.retryPendingSandboxTeardown({ environment: null, lease: lease! }); + expect(destroySpy).toHaveBeenCalledWith( + expect.objectContaining({ providerLeaseId: "sandbox://fake/delete-after-record" }), + ); + } finally { + destroySpy.mockRestore(); + } + }); + + // A plugin provider whose config declares an `apiKey` secret-ref field. The + // orphan teardown must resolve that recorded ref even when the environment + // binding is gone, so these tests register a provider with a real secret-ref + // field, unlike the plain `fake-plugin` tests above. + const SECRET_REF_PLUGIN_KEY = "paperclip.secret-plugin-sandbox-provider"; + const SECRET_REF_PROVIDER = "secret-plugin"; + + async function registerSecretRefPluginProvider(): Promise { + const pluginId = randomUUID(); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: SECRET_REF_PLUGIN_KEY, + packageName: "@paperclipai/plugin-secret-sandbox", + version: "1.0.0", + apiVersion: 1, + categories: ["automation"], + manifestJson: { + id: SECRET_REF_PLUGIN_KEY, + apiVersion: 1, + version: "1.0.0", + displayName: "Secret Plugin Sandbox Provider", + description: "Test plugin provider with a secret-ref config field", + author: "Paperclip", + categories: ["automation"], + capabilities: ["environment.drivers.register"], + entrypoints: { worker: "dist/worker.js" }, + environmentDrivers: [ + { + driverKey: SECRET_REF_PROVIDER, + kind: "sandbox_provider", + displayName: "Secret Plugin", + configSchema: { + type: "object", + properties: { + apiKey: { type: "string", format: "secret-ref" }, + }, + }, + }, + ], + }, + status: "ready", + installOrder: 1, + updatedAt: new Date(), + } as any); + return pluginId; + } + + // A worker manager mock that records the config of each destroy call, so a + // test can assert the resolved API key reached the provider teardown. + function createDestroyRecordingWorkerManager(pluginId: string): { + workerManager: PluginWorkerManager; + destroyConfigs: Array | undefined>; + } { + const destroyConfigs: Array | undefined> = []; + const workerManager = { + isRunning: vi.fn((id: string) => id === pluginId), + call: vi.fn(async (_pluginId: string, method: string, args: any) => { + if (method === "environmentDestroyLease") { + destroyConfigs.push(args?.config as Record | undefined); + return {}; + } + throw new Error(`Unexpected plugin method: ${method}`); + }), + } as unknown as PluginWorkerManager; + return { workerManager, destroyConfigs }; + } + + async function createApiKeySecret(companyId: string, value: string): Promise { + const secret = await secretService(db).create(companyId, { + name: `sandbox-cleanup-api-key-${randomUUID()}`, + provider: "local_encrypted", + value, + }); + return secret.id; + } + + it("tears down a secret-backed plugin orphan when a delete already removed the environment", async () => { + // Ordering: a delete removed the environment (and its secret binding) before + // the acquire recorded the orphan. The recorded config keeps the API-key + // secret ref, so the teardown must resolve it without the environment + // binding and send the resolved value to the provider destroy call. + const pluginId = await registerSecretRefPluginProvider(); + const { companyId, environment } = await seedEnvironment({ + driver: "sandbox", + name: "Secret Plugin Delete Before Record", + config: { provider: SECRET_REF_PROVIDER, reuseLease: false }, + }); + const secretId = await createApiKeySecret(companyId, "old-provider-api-key"); + await secretService(db).createBinding({ + companyId, + secretId, + targetType: "environment", + targetId: environment.id, + configPath: "apiKey", + }); + + // Remove the environment first, so the record happens after the delete. + await environmentService(db).remove(environment.id); + + const orphan = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + provider: SECRET_REF_PROVIDER, + providerLeaseId: "secret-plugin-lease-delete-before", + metadata: { + provider: SECRET_REF_PROVIDER, + driver: "sandbox", + sandboxProviderPlugin: true, + pluginId, + pluginKey: SECRET_REF_PLUGIN_KEY, + apiKey: secretId, + reuseLease: false, + }, + failureReason: "acquire_rejected_teardown_failed", + }); + expect(orphan.environmentId).toBeNull(); + expect(orphan.status).toBe("pending_cleanup"); + + const { workerManager, destroyConfigs } = createDestroyRecordingWorkerManager(pluginId); + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + + const lease = await environmentService(db).getLeaseById(orphan.id); + await runtimeWithPlugin.retryPendingSandboxTeardown({ environment: null, lease: lease! }); + expect(destroyConfigs).toHaveLength(1); + // The recorded secret ref resolved to the old credential, and the resolved + // value reached the provider teardown instead of the secret ref. + expect(destroyConfigs[0]).toMatchObject({ apiKey: "old-provider-api-key" }); + expect(destroyConfigs[0]?.apiKey).not.toBe(secretId); + }); + + it("tears down a secret-backed plugin orphan after the environment is deleted", async () => { + // Ordering: the orphan is recorded, then a delete removes the environment. + // The foreign key sets the lease reference null, so the orphan survives. The + // teardown resolves the recorded API-key ref without the environment. + const pluginId = await registerSecretRefPluginProvider(); + const { companyId, environment } = await seedEnvironment({ + driver: "sandbox", + name: "Secret Plugin Delete After Record", + config: { provider: SECRET_REF_PROVIDER, reuseLease: false }, + }); + const secretId = await createApiKeySecret(companyId, "old-provider-api-key"); + await secretService(db).createBinding({ + companyId, + secretId, + targetType: "environment", + targetId: environment.id, + configPath: "apiKey", + }); + + const orphan = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + provider: SECRET_REF_PROVIDER, + providerLeaseId: "secret-plugin-lease-delete-after", + metadata: { + provider: SECRET_REF_PROVIDER, + driver: "sandbox", + sandboxProviderPlugin: true, + pluginId, + pluginKey: SECRET_REF_PLUGIN_KEY, + apiKey: secretId, + reuseLease: false, + }, + failureReason: "acquire_rejected_teardown_failed", + }); + expect(orphan.environmentId).toBe(environment.id); + + // Delete the environment. The foreign key sets the lease reference null, so + // the orphan survives and the current binding is gone. + await environmentService(db).remove(environment.id); + const survived = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.id, orphan.id)) + .then((r) => r[0]!); + expect(survived.environmentId).toBeNull(); + + const { workerManager, destroyConfigs } = createDestroyRecordingWorkerManager(pluginId); + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + + const lease = await environmentService(db).getLeaseById(orphan.id); + await runtimeWithPlugin.retryPendingSandboxTeardown({ environment: null, lease: lease! }); + expect(destroyConfigs).toHaveLength(1); + expect(destroyConfigs[0]).toMatchObject({ apiKey: "old-provider-api-key" }); + }); + + it("tears down a secret-backed plugin orphan with the recorded credential after the provider is reconfigured", async () => { + // Ordering: the orphan is recorded for the old credential, then the + // environment is reconfigured to a new credential. The environment binding + // now points to a different secret at the same path. The teardown must + // resolve the OLD recorded ref, not the new binding, so it destroys the + // sandbox the old credential provisioned. + const pluginId = await registerSecretRefPluginProvider(); + const { companyId, environment } = await seedEnvironment({ + driver: "sandbox", + name: "Secret Plugin Reconfigured", + config: { provider: SECRET_REF_PROVIDER, reuseLease: false }, + }); + const oldSecretId = await createApiKeySecret(companyId, "old-provider-api-key"); + + const orphan = await environmentService(db).insertPendingCleanupLease({ + companyId, + environmentId: environment.id, + provider: SECRET_REF_PROVIDER, + providerLeaseId: "secret-plugin-lease-reconfigured", + metadata: { + provider: SECRET_REF_PROVIDER, + driver: "sandbox", + sandboxProviderPlugin: true, + pluginId, + pluginKey: SECRET_REF_PLUGIN_KEY, + apiKey: oldSecretId, + reuseLease: false, + }, + failureReason: "acquire_rejected_teardown_failed", + }); + expect(orphan.environmentId).toBe(environment.id); + + // Reconfigure the environment: a new secret replaces the binding at the same + // path. The old recorded ref is no longer bound to the environment. + const newSecretId = await createApiKeySecret(companyId, "new-provider-api-key"); + await secretService(db).createBinding({ + companyId, + secretId: newSecretId, + targetType: "environment", + targetId: environment.id, + configPath: "apiKey", + }); + await environmentService(db).update(environment.id, { + driver: "sandbox", + config: { provider: SECRET_REF_PROVIDER, apiKey: newSecretId, reuseLease: false }, + }); + + const { workerManager, destroyConfigs } = createDestroyRecordingWorkerManager(pluginId); + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + + const lease = await environmentService(db).getLeaseById(orphan.id); + await runtimeWithPlugin.retryPendingSandboxTeardown({ environment: null, lease: lease! }); + expect(destroyConfigs).toHaveLength(1); + // The teardown resolved the OLD recorded credential, not the new binding. + expect(destroyConfigs[0]).toMatchObject({ apiKey: "old-provider-api-key" }); + expect(destroyConfigs[0]?.apiKey).not.toBe("new-provider-api-key"); + + await environmentService(db).update(environment.id, { driver: "local", config: {} }); + }); + + it("tears the recorded plugin provider lease down for a pending-cleanup orphan", async () => { + const pluginId = randomUUID(); + const { companyId, environment: baseEnvironment, runId } = await seedEnvironment(); + const pluginConfig = { provider: "fake-plugin", image: "fake:test", reuseLease: false }; + const environment = { + ...baseEnvironment, + name: "Cleanup Retry Plugin", + driver: "sandbox", + config: pluginConfig, + }; + await environmentService(db).update(environment.id, { + driver: "sandbox", + name: environment.name, + config: pluginConfig, + }); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclip.fake-plugin-sandbox-provider", + packageName: "@paperclipai/plugin-fake-sandbox", + version: "1.0.0", + apiVersion: 1, + categories: ["automation"], + manifestJson: { + id: "paperclip.fake-plugin-sandbox-provider", + apiVersion: 1, + version: "1.0.0", + displayName: "Fake Plugin Sandbox Provider", + description: "Test fake plugin provider", + author: "Paperclip", + categories: ["automation"], + capabilities: ["environment.drivers.register"], + entrypoints: { worker: "dist/worker.js" }, + environmentDrivers: [ + { + driverKey: "fake-plugin", + kind: "sandbox_provider", + displayName: "Fake Plugin", + configSchema: { type: "object" }, + }, + ], + }, + status: "ready", + installOrder: 1, + updatedAt: new Date(), + } as any); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co Retry Plugin", + issuePrefix: "OTG", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(builtInManagedResources).values({ + companyId: otherCompanyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environment.id, + stockVersion: "1", + stockHash: "hash", + }); + + // The first destroy fails, so the acquire records a pending-cleanup orphan. + // The sweep retries the destroy, which succeeds this time. + let destroyAttempts = 0; + const workerManager = { + isRunning: vi.fn((id: string) => id === pluginId), + getWorker: vi.fn(() => ({ supportedMethods: ["environmentDestroyLease"] })), + call: vi.fn(async (_pluginId: string, method: string) => { + if (method === "environmentAcquireLease") { + return { + providerLeaseId: "plugin-lease-3", + metadata: { provider: "fake-plugin", image: "fake:test", reuseLease: false }, + }; + } + if (method === "environmentDestroyLease") { + destroyAttempts += 1; + if (destroyAttempts === 1) { + throw new Error("destroy failed"); + } + return {}; + } + throw new Error(`Unexpected plugin method: ${method}`); + }), + } as unknown as PluginWorkerManager; + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + + await expect( + runtimeWithPlugin.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + assertCompanyBinding: true, + }), + ).rejects.toMatchObject({ status: 403, details: { code: "environment_company_mismatch" } }); + + const orphan = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environment.id)) + .then((r) => r[0]!); + expect(orphan.status).toBe("pending_cleanup"); + expect(orphan.providerLeaseId).toBe("plugin-lease-3"); + + const lease = await environmentService(db).getLeaseById(orphan.id); + await runtimeWithPlugin.retryPendingSandboxTeardown({ environment: null, lease: lease! }); + expect(destroyAttempts).toBe(2); + const destroyCall = (workerManager.call as unknown as ReturnType).mock.calls + .filter((callArgs) => callArgs[1] === "environmentDestroyLease") + .at(-1); + expect(destroyCall?.[2]).toMatchObject({ providerLeaseId: "plugin-lease-3" }); + + await environmentService(db).update(environment.id, { driver: "local", config: {} }); + }); + it("uses plugin-backed sandbox config for execute and release", async () => { const pluginId = randomUUID(); const { companyId, environment: baseEnvironment, runId } = await seedEnvironment(); @@ -3330,6 +5639,71 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { }); }); + it("sweeps reusable cleanup with the configuration recorded on the lease", async () => { + const { pluginId, environment, reusableLease } = await seedReusablePluginSandboxLease(); + const environmentsSvc = environmentService(db); + await environmentsSvc.releaseLease(reusableLease.id, "pending_cleanup", { + failureReason: "release_cleanup_failed", + cleanupStatus: "failed", + }); + + // Re-point the environment after the reusable sandbox was provisioned. + // The pending-cleanup sweep still enters destroyRunLease because the + // environment exists, but destroy must target the provider and config + // captured on the lease rather than this current configuration. + await environmentsSvc.update(environment.id, { + config: { + provider: "replacement-provider", + image: "replacement:test", + timeoutMs: 9999, + reuseLease: true, + }, + }); + + const workerManager = { + isRunning: vi.fn((id: string) => id === pluginId), + call: vi.fn(async (_pluginId: string, method: string) => { + if (method === "environmentDestroyLease") return undefined; + throw new Error(`Unexpected plugin method: ${method}`); + }), + getWorker: vi.fn(() => ({ + supportedMethods: ["environmentResumeLease", "environmentReleaseLease", "environmentDestroyLease"], + })), + } as unknown as PluginWorkerManager; + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + const heartbeat = heartbeatService(db, { environmentRuntime: runtimeWithPlugin }); + + const result = await heartbeat.sweepPendingCleanupLeases({ backoffMs: 0 }); + + expect(result).toEqual({ swept: 1, destroyed: 1, capped: 0 }); + expect(workerManager.call).toHaveBeenCalledWith( + pluginId, + "environmentDestroyLease", + expect.objectContaining({ + driverKey: "fake-plugin", + environmentId: environment.id, + providerLeaseId: "reusable-plugin-lease", + config: expect.objectContaining({ + image: "fake:test", + timeoutMs: 1234, + reuseLease: true, + }), + }), + 31234, + ); + const destroyInput = vi.mocked(workerManager.call).mock.calls[0]?.[2] as { + config?: Record; + }; + expect(destroyInput.config).not.toMatchObject({ + image: "replacement:test", + timeoutMs: 9999, + }); + await expect(environmentsSvc.getLeaseById(reusableLease.id)).resolves.toMatchObject({ + status: "expired", + cleanupStatus: "success", + }); + }); + it("retries reusable plugin-backed sandbox destroy when the worker is unavailable", async () => { const { pluginId, companyId, executionWorkspaceId, reusableLease } = await seedReusablePluginSandboxLease(); @@ -3945,6 +6319,111 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { expect(released[0]?.lease.status).toBe("released"); }); + async function seedPluginDriverEnvironment(pluginId: string) { + const seeded = await seedEnvironment({ + driver: "plugin", + name: "Plugin Fake plugin", + config: { + pluginKey: "acme.environments", + driverKey: "fake-plugin", + driverConfig: { template: "base" }, + }, + }); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "acme.environments", + packageName: "@acme/paperclip-environments", + version: "1.0.0", + apiVersion: 1, + categories: ["automation"], + manifestJson: { + id: "acme.environments", + apiVersion: 1, + version: "1.0.0", + displayName: "Acme Environments", + description: "Test plugin environment driver", + author: "Acme", + categories: ["automation"], + capabilities: ["environment.drivers.register"], + entrypoints: { worker: "dist/worker.js" }, + environmentDrivers: [ + { driverKey: "fake-plugin", displayName: "Fake plugin", configSchema: { type: "object" } }, + ], + }, + status: "ready", + installOrder: 1, + updatedAt: new Date(), + } as any); + return seeded; + } + + it("forwards a requested expiry to the acquire RPC and records only a provider-attested expiry", async () => { + const pluginId = randomUUID(); + let receivedRequestedExpiresAt: unknown; + const workerManager = { + isRunning: vi.fn(() => true), + call: vi.fn(async (_pluginId: string, method: string, params: Record) => { + if (method === "environmentAcquireLease") { + receivedRequestedExpiresAt = params.requestedExpiresAt; + // The provider grants no expiry. + return { providerLeaseId: "plugin-lease-no-expiry", metadata: { remoteCwd: "/workspace" } }; + } + return undefined; + }), + } as unknown as PluginWorkerManager; + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + const { companyId, environment, runId } = await seedPluginDriverEnvironment(pluginId); + + const requestedExpiresAt = new Date(Date.now() + 60_000); + const acquired = await runtimeWithPlugin.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + requestedExpiresAt, + }); + + // The acquire RPC receives the requested expiry as an ISO 8601 string. + expect(receivedRequestedExpiresAt).toBe(requestedExpiresAt.toISOString()); + // The provider gave no expiry, so the lease row records no expiry. The runtime + // never synthesizes the requested deadline as evidence of provider enforcement. + expect(acquired.lease.expiresAt ?? null).toBeNull(); + }); + + it("records the real provider expiry when the provider grants one later than the requested deadline", async () => { + const pluginId = randomUUID(); + const providerExpiresAt = new Date(Date.now() + 120_000).toISOString(); + const workerManager = { + isRunning: vi.fn(() => true), + call: vi.fn(async (_pluginId: string, method: string) => { + if (method === "environmentAcquireLease") { + return { + providerLeaseId: "plugin-lease-later", + expiresAt: providerExpiresAt, + metadata: { remoteCwd: "/workspace" }, + }; + } + return undefined; + }), + } as unknown as PluginWorkerManager; + const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + const { companyId, environment, runId } = await seedPluginDriverEnvironment(pluginId); + + const acquired = await runtimeWithPlugin.acquireRunLease({ + companyId, + environment, + issueId: null, + heartbeatRunId: runId, + persistedExecutionWorkspace: null, + requestedExpiresAt: new Date(Date.now() + 60_000), + }); + + // The lease row records the real provider expiry, not the requested deadline. + // The caller then verifies the expiry bounds its deadline and fails closed. + expect(acquired.lease.expiresAt?.toISOString()).toBe(providerExpiresAt); + }); + it("delegates the full plugin environment lifecycle through the worker manager", async () => { const pluginId = randomUUID(); const workerManager = { diff --git a/server/src/__tests__/environment-service.test.ts b/server/src/__tests__/environment-service.test.ts index 9f294cb311..1f77984a71 100644 --- a/server/src/__tests__/environment-service.test.ts +++ b/server/src/__tests__/environment-service.test.ts @@ -397,6 +397,8 @@ describeEmbeddedPostgres("environmentService leases", () => { environmentId, canDelete: false, deleteBlockedReasons: ["instance_default"], + pendingCleanupLeaseCount: 0, + reusableSandboxLeaseCount: 0, staticReferences: { isManagedLocal: false, isInstanceDefault: true, @@ -488,6 +490,374 @@ describeEmbeddedPostgres("environmentService leases", () => { expect(deletedRows).toHaveLength(0); }); + it("blocks delete while a pending_cleanup lease still holds the sandbox reference", async () => { + const companyId = randomUUID(); + const environmentId = randomUUID(); + const now = new Date(); + await db.insert(companies).values({ + id: companyId, + name: "Acme", + status: "active", + createdAt: now, + updatedAt: now, + }); + await db.insert(environments).values({ + id: environmentId, + name: "Pending Cleanup Guard", + driver: "ssh", + status: "active", + config: { + host: "pending.example.test", + port: 22, + username: "fixture", + remoteWorkspacePath: "/srv/paperclip", + }, + createdAt: now, + updatedAt: now, + }); + await svc.insertPendingCleanupLease({ + companyId, + environmentId, + provider: "fake-plugin", + providerLeaseId: "sandbox-1", + failureReason: "teardown_failed", + }); + + const impact = await svc.getDeleteBlastRadius(environmentId); + expect(impact?.canDelete).toBe(false); + expect(impact?.deleteBlockedReasons).toContain("pending_sandbox_cleanup"); + expect(impact?.pendingCleanupLeaseCount).toBe(1); + expect(await svc.hasUnresolvedPendingCleanupLeases(environmentId)).toBe(true); + + const removed = await svc.removeIfDeletable(environmentId); + const rows = await db.select().from(environments).where(eq(environments.id, environmentId)); + expect(removed).toBeNull(); + expect(rows).toHaveLength(1); + }); + + it("atomically blocks delete while a reusable sandbox lease is still live", async () => { + const companyId = randomUUID(); + const environmentId = randomUUID(); + const now = new Date(); + await db.insert(companies).values({ + id: companyId, + name: "Acme", + status: "active", + createdAt: now, + updatedAt: now, + }); + await db.insert(environments).values({ + id: environmentId, + name: "Reusable Sandbox Guard", + driver: "sandbox", + status: "active", + config: { + provider: "fake", + image: "ubuntu:24.04", + reuseLease: true, + }, + createdAt: now, + updatedAt: now, + }); + const lease = await svc.acquireLease({ + companyId, + environmentId, + leasePolicy: "reuse_by_environment", + provider: "fake", + providerLeaseId: "sandbox-reusable-1", + metadata: { + driver: "sandbox", + provider: "fake", + image: "ubuntu:24.04", + reuseLease: true, + }, + }); + + const activeImpact = await svc.getDeleteBlastRadius(environmentId); + expect(activeImpact?.canDelete).toBe(false); + expect(activeImpact?.deleteBlockedReasons).toContain("reusable_sandbox_lease"); + expect(activeImpact?.reusableSandboxLeaseCount).toBe(1); + expect(await svc.removeIfDeletable(environmentId)).toBeNull(); + + // A released reusable lease still owns a provider sandbox that may be + // resumed, so it must remain protected until scoped cleanup destroys it. + await svc.releaseLease(lease.id, "released"); + const releasedImpact = await svc.getDeleteBlastRadius(environmentId); + expect(releasedImpact?.canDelete).toBe(false); + expect(releasedImpact?.reusableSandboxLeaseCount).toBe(1); + expect(await svc.removeIfDeletable(environmentId)).toBeNull(); + + const rows = await db.select().from(environments).where(eq(environments.id, environmentId)); + expect(rows).toHaveLength(1); + const storedLease = await svc.getLeaseById(lease.id); + expect(storedLease?.environmentId).toBe(environmentId); + }); + + it("allows delete with an active ephemeral lease", async () => { + const companyId = randomUUID(); + const environmentId = randomUUID(); + const now = new Date(); + await db.insert(companies).values({ + id: companyId, + name: "Acme", + status: "active", + createdAt: now, + updatedAt: now, + }); + await db.insert(environments).values({ + id: environmentId, + name: "Ephemeral Sandbox", + driver: "sandbox", + status: "active", + config: { provider: "fake", image: "ubuntu:24.04" }, + createdAt: now, + updatedAt: now, + }); + await svc.acquireLease({ + companyId, + environmentId, + leasePolicy: "ephemeral", + provider: "fake", + providerLeaseId: "sandbox-ephemeral-1", + }); + + const impact = await svc.getDeleteBlastRadius(environmentId); + expect(impact?.activeRuntimeUse.activeLeaseCount).toBe(1); + expect(impact?.reusableSandboxLeaseCount).toBe(0); + expect(impact?.canDelete).toBe(true); + expect((await svc.removeIfDeletable(environmentId))?.id).toBe(environmentId); + }); + + it("allows delete once the pending_cleanup lease resolves", async () => { + const companyId = randomUUID(); + const environmentId = randomUUID(); + const now = new Date(); + await db.insert(companies).values({ + id: companyId, + name: "Acme", + status: "active", + createdAt: now, + updatedAt: now, + }); + await db.insert(environments).values({ + id: environmentId, + name: "Resolved Cleanup Guard", + driver: "ssh", + status: "active", + config: { + host: "resolved.example.test", + port: 22, + username: "fixture", + remoteWorkspacePath: "/srv/paperclip", + }, + createdAt: now, + updatedAt: now, + }); + const lease = await svc.insertPendingCleanupLease({ + companyId, + environmentId, + provider: "fake-plugin", + providerLeaseId: "sandbox-1", + failureReason: "teardown_failed", + }); + // The sweep marks a cleaned orphan `expired`, so it leaves the terminal + // `pending_cleanup` state and no longer blocks the delete. + await svc.releaseLease(lease.id, "expired", { cleanupStatus: "success" }); + + const impact = await svc.getDeleteBlastRadius(environmentId); + expect(impact?.canDelete).toBe(true); + expect(impact?.pendingCleanupLeaseCount).toBe(0); + expect(impact?.reusableSandboxLeaseCount).toBe(0); + expect(await svc.hasUnresolvedPendingCleanupLeases(environmentId)).toBe(false); + + const removed = await svc.removeIfDeletable(environmentId); + const rows = await db.select().from(environments).where(eq(environments.id, environmentId)); + expect(removed?.id).toBe(environmentId); + expect(rows).toHaveLength(0); + }); + + it("records the orphan with a null reference when a delete already removed the environment", async () => { + // A delete wins the untracked window before the orphan record lands. The + // record must still persist, so the sweep keeps a durable handle to the + // remote sandbox. + const companyId = randomUUID(); + const environmentId = randomUUID(); + const now = new Date(); + await db.insert(companies).values({ + id: companyId, + name: "Acme", + status: "active", + createdAt: now, + updatedAt: now, + }); + await db.insert(environments).values({ + id: environmentId, + name: "Deleted Before Record", + driver: "ssh", + status: "active", + config: { + host: "deleted.example.test", + port: 22, + username: "fixture", + remoteWorkspacePath: "/srv/paperclip", + }, + createdAt: now, + updatedAt: now, + }); + + const removed = await svc.removeIfDeletable(environmentId); + expect(removed?.id).toBe(environmentId); + + // The foreign key does not reject the insert. The insert reads the absent + // environment and stores a null reference with the immutable provider + // metadata, so the teardown still runs later. + const lease = await svc.insertPendingCleanupLease({ + companyId, + environmentId, + provider: "fake-plugin", + providerLeaseId: "sandbox-orphan-1", + metadata: { provider: "fake-plugin", config: { provider: "fake-plugin" } }, + failureReason: "acquire_rejected_teardown_failed", + }); + + expect(lease.environmentId).toBeNull(); + expect(lease.status).toBe("pending_cleanup"); + expect(lease.provider).toBe("fake-plugin"); + expect(lease.providerLeaseId).toBe("sandbox-orphan-1"); + + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.id, lease.id)); + expect(rows).toHaveLength(1); + expect(rows[0]?.environmentId).toBeNull(); + expect(rows[0]?.metadata).toMatchObject({ provider: "fake-plugin" }); + }); + + it("keeps the orphan when an environment delete and the orphan record race", async () => { + // Race the delete against the orphan record. The record locks the + // environment row `for update`, and the delete refuses while a + // `pending_cleanup` lease exists, so the two writes serialize into one of + // two safe outcomes. The orphan row persists in both. + const companyId = randomUUID(); + const environmentId = randomUUID(); + const now = new Date(); + await db.insert(companies).values({ + id: companyId, + name: "Acme", + status: "active", + createdAt: now, + updatedAt: now, + }); + await db.insert(environments).values({ + id: environmentId, + name: "Delete Vs Record Race", + driver: "ssh", + status: "active", + config: { + host: "race.example.test", + port: 22, + username: "fixture", + remoteWorkspacePath: "/srv/paperclip", + }, + createdAt: now, + updatedAt: now, + }); + + const [removed, lease] = await Promise.all([ + svc.removeIfDeletable(environmentId), + svc.insertPendingCleanupLease({ + companyId, + environmentId, + provider: "fake-plugin", + providerLeaseId: "sandbox-orphan-2", + metadata: { provider: "fake-plugin", config: { provider: "fake-plugin" } }, + failureReason: "acquire_rejected_teardown_failed", + }), + ]); + + // The orphan row always persists, so no ordering strands the sandbox. + const leaseRows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.status, "pending_cleanup")); + expect(leaseRows).toHaveLength(1); + expect(leaseRows[0]?.id).toBe(lease.id); + expect(leaseRows[0]?.provider).toBe("fake-plugin"); + + const environmentRows = await db + .select() + .from(environments) + .where(eq(environments.id, environmentId)); + if (removed) { + // The delete won. The environment is gone, and the orphan keeps a null + // reference with its immutable provider metadata. + expect(environmentRows).toHaveLength(0); + expect(leaseRows[0]?.environmentId).toBeNull(); + } else { + // The record won. The orphan keeps the environment reference, and the + // delete refused because the `pending_cleanup` lease now exists. + expect(environmentRows).toHaveLength(1); + expect(leaseRows[0]?.environmentId).toBe(environmentId); + } + }); + + it("keeps the recorded provider immutable when a provider change and the orphan record race", async () => { + // Race a provider change against the orphan record for the original + // provider. The recorded lease must keep the original provider and its + // immutable teardown metadata, so a later retry targets the original + // provider, never the reconfigured one. + const companyId = randomUUID(); + const environmentId = randomUUID(); + const now = new Date(); + await db.insert(companies).values({ + id: companyId, + name: "Acme", + status: "active", + createdAt: now, + updatedAt: now, + }); + await db.insert(environments).values({ + id: environmentId, + name: "Provider Change Vs Record Race", + driver: "sandbox", + status: "active", + config: { provider: "provider-a" }, + createdAt: now, + updatedAt: now, + }); + + const [, lease] = await Promise.all([ + svc.update(environmentId, { + driver: "sandbox", + config: { provider: "provider-b" }, + }), + svc.insertPendingCleanupLease({ + companyId, + environmentId, + provider: "provider-a", + providerLeaseId: "sandbox-orphan-3", + metadata: { provider: "provider-a", config: { provider: "provider-a" } }, + failureReason: "acquire_rejected_teardown_failed", + }), + ]); + + const rows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.id, lease.id)); + expect(rows[0]?.provider).toBe("provider-a"); + expect(rows[0]?.metadata).toMatchObject({ provider: "provider-a" }); + + // The environment now points at the reconfigured provider, but the recorded + // teardown context stays independent of it. + const environmentRows = await db + .select() + .from(environments) + .where(eq(environments.id, environmentId)); + expect((environmentRows[0]?.config as { provider?: string } | null)?.provider).toBe("provider-b"); + }); + it("creates and then reuses the default local environment for a company", async () => { const companyId = randomUUID(); await db.insert(companies).values({ @@ -783,6 +1153,47 @@ describeEmbeddedPostgres("environmentService leases", () => { expect(rows).toHaveLength(1); }); + it("lists the companies that own a managed sandbox environment", async () => { + const companyA = randomUUID(); + const companyB = randomUUID(); + await db.insert(companies).values([ + { id: companyA, name: "Company A", status: "active", issuePrefix: "CA", createdAt: new Date(), updatedAt: new Date() }, + { id: companyB, name: "Company B", status: "active", issuePrefix: "CB", createdAt: new Date(), updatedAt: new Date() }, + ]); + // Two companies provision the same managed sandbox slot, so both bind to the + // one environment row. + const created = await svc.ensureManagedSandboxEnvironment({ + companyId: companyA, + name: "Daytona", + provider: "daytona", + config: { target: "us" }, + }); + const environmentId = created.environment.id; + await svc.ensureManagedSandboxEnvironment({ + companyId: companyB, + name: "Daytona", + provider: "daytona", + config: { target: "us" }, + }); + + const owners = await svc.listBoundCompanyIds(environmentId); + expect(owners.slice().sort()).toEqual([companyA, companyB].sort()); + + // An operator-created environment has no managed binding, so it lists no + // owner and stays instance-global. + const unboundId = randomUUID(); + await db.insert(environments).values({ + id: unboundId, + name: "Operator sandbox", + driver: "sandbox", + status: "active", + config: { provider: "kubernetes" }, + createdAt: new Date(), + updatedAt: new Date(), + }); + expect(await svc.listBoundCompanyIds(unboundId)).toEqual([]); + }); + it("preserves tenant env vars across managed sandbox boot reconciles", async () => { // The cloud contract lets tenants add env vars — and only env vars — // to the managed sandbox environment. The provisioner reconciles @@ -1348,4 +1759,104 @@ describeEmbeddedPostgres("environmentService leases", () => { const rows = await db.select().from(environments); expect(rows.filter((row) => row.driver === "ssh")).toHaveLength(2); }); + + describe("acquireLease company-binding guard", () => { + async function seedSandboxEnvironment(name = "Managed Sandbox") { + const companyId = randomUUID(); + const environmentId = randomUUID(); + await db.insert(companies).values({ + id: companyId, + name, + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(environments).values({ + id: environmentId, + name, + driver: "sandbox", + status: "active", + config: { provider: "daytona" }, + createdAt: new Date(), + updatedAt: new Date(), + }); + return { companyId, environmentId }; + } + + async function bindEnvironmentToCompany(environmentId: string, companyId: string) { + await db.insert(builtInManagedResources).values({ + companyId, + bundleKey: "managed-environment", + resourceKind: "environment", + resourceKey: "managed-sandbox", + resourceId: environmentId, + stockVersion: "1", + stockHash: "hash", + }); + } + + it("rejects an acquire when a bind lands after the route check", async () => { + const { companyId, environmentId } = await seedSandboxEnvironment(); + // The other company owns a managed binding that a reconciliation committed + // after the route guard read an empty binding list. + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co", + issuePrefix: "OTA", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await bindEnvironmentToCompany(environmentId, otherCompanyId); + + await expect( + svc.acquireLease({ companyId, environmentId, assertCompanyBinding: true }), + ).rejects.toMatchObject({ + status: 403, + details: { code: "environment_company_mismatch" }, + }); + // The insert never ran, so the caller holds no lease. + const leaseRows = await db + .select() + .from(environmentLeases) + .where(eq(environmentLeases.environmentId, environmentId)); + expect(leaseRows).toHaveLength(0); + }); + + it("acquires when the environment is unbound (instance-global)", async () => { + const { companyId, environmentId } = await seedSandboxEnvironment("Unbound Sandbox"); + + const lease = await svc.acquireLease({ companyId, environmentId, assertCompanyBinding: true }); + expect(lease.status).toBe("active"); + expect(lease.companyId).toBe(companyId); + }); + + it("acquires when the environment is bound to the caller company", async () => { + const { companyId, environmentId } = await seedSandboxEnvironment("Own Sandbox"); + await bindEnvironmentToCompany(environmentId, companyId); + + const lease = await svc.acquireLease({ companyId, environmentId, assertCompanyBinding: true }); + expect(lease.status).toBe("active"); + }); + + it("does not check the binding for callers that omit the flag", async () => { + const { companyId, environmentId } = await seedSandboxEnvironment("Legacy Path Sandbox"); + const otherCompanyId = randomUUID(); + await db.insert(companies).values({ + id: otherCompanyId, + name: "Other Co 2", + issuePrefix: "OTB", + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + await bindEnvironmentToCompany(environmentId, otherCompanyId); + + // The heartbeat run path does not set the flag, so the binding check does + // not run and the lease acquires as before. + const lease = await svc.acquireLease({ companyId, environmentId }); + expect(lease.status).toBe("active"); + }); + }); }); diff --git a/server/src/__tests__/fixtures/plugin-worker-setup-token-pty.cjs b/server/src/__tests__/fixtures/plugin-worker-setup-token-pty.cjs new file mode 100644 index 0000000000..31b8ca6c38 --- /dev/null +++ b/server/src/__tests__/fixtures/plugin-worker-setup-token-pty.cjs @@ -0,0 +1,161 @@ +// Test worker fixture for the host-owned setup-token login pseudo-terminal route +// gate. The fixture drives the manager route state machine through +// the four typed methods (open, input, stop, close) and the output and exit +// notifications. +// +// The manager allowlists `command` to the fixed `CLAUDE_SETUP_TOKEN_COMMAND`. The +// test encodes a JSON directive in the forwarded `providerLeaseId`, so one fixture +// serves every route-gate case: +// - `mode`: "normal" | "malformed-open" | "no-open-reply" | "duplicate-open-reply" +// - `workerSessionId`: the worker session id the open reply returns (default "ws-1") +// - `outputs`: an array of `{ chunk, sid? }`. The fixture emits each as an output +// notification after the open reply. `sid` defaults to the real worker session +// id; a test sets a wrong `sid` to prove the host drops a mismatched +// notification. +// - `exitCode`: when set, the fixture emits an exit notification after the outputs. +// - `closeMode`: "ack" | "bad-ack" | "no-ack" (default "ack"). It controls the +// close reply, so a test proves the host retires the worker on an unconfirmed +// close. +const readline = require("node:readline"); + +function send(message) { + process.stdout.write(`${JSON.stringify(message)}\n`); +} + +// The registered terminals, keyed by the host route id. Each entry records the +// bound worker session id and the close directive. +const routes = new Map(); + +function parseDirective(raw) { + try { + const parsed = JSON.parse(raw); + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } +} + +const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); + +rl.on("line", (line) => { + if (!line.trim()) return; + const message = JSON.parse(line); + const method = message && typeof message.method === "string" ? message.method : null; + const params = message.params ?? {}; + + if (method === "initialize") { + send({ + jsonrpc: "2.0", + id: message.id, + result: { + ok: true, + supportedMethods: [ + "setupTokenPtyOpen", + "setupTokenPtyInput", + "setupTokenPtyStop", + "setupTokenPtyClose", + ], + }, + }); + return; + } + + if (method === "setupTokenPtyOpen") { + const directive = parseDirective(params.providerLeaseId); + const mode = directive.mode ?? "normal"; + const workerSessionId = directive.workerSessionId ?? "ws-1"; + const closeMode = directive.closeMode ?? "ack"; + routes.set(params.hostRouteId, { workerSessionId, closeMode }); + + if (mode === "no-open-reply") { + // Never reply, so the host open call times out. + return; + } + if (mode === "malformed-open") { + // Reply with no worker session id, so the host terminalizes the route. + send({ jsonrpc: "2.0", id: message.id, result: {} }); + return; + } + + const reply = () => + send({ jsonrpc: "2.0", id: message.id, result: { workerSessionId } }); + reply(); + if (mode === "duplicate-open-reply") { + // Send a second open reply for the same request id. The host drops it. + reply(); + } + + // Emit the scripted output and the exit after the open reply, so the host + // binds the route first. + setImmediate(() => { + const outputs = Array.isArray(directive.outputs) ? directive.outputs : []; + for (const entry of outputs) { + send({ + jsonrpc: "2.0", + method: "setupTokenPty.output", + params: { + workerSessionId: entry.sid ?? workerSessionId, + chunk: entry.chunk, + }, + }); + } + if (typeof directive.exitCode === "number") { + send({ + jsonrpc: "2.0", + method: "setupTokenPty.exit", + params: { workerSessionId, exitCode: directive.exitCode }, + }); + } + }); + return; + } + + if (method === "setupTokenPtyInput") { + // Echo the input back as one output notification for the bound session, so a + // test proves the input reaches the worker and the output routes back. + for (const entry of routes.values()) { + if (entry.workerSessionId === params.workerSessionId) { + send({ + jsonrpc: "2.0", + method: "setupTokenPty.output", + params: { workerSessionId: entry.workerSessionId, chunk: `echo:${params.data}` }, + }); + } + } + send({ jsonrpc: "2.0", id: message.id, result: null }); + return; + } + + if (method === "setupTokenPtyStop") { + send({ jsonrpc: "2.0", id: message.id, result: null }); + return; + } + + if (method === "setupTokenPtyClose") { + const entry = routes.get(params.hostRouteId); + routes.delete(params.hostRouteId); + const closeMode = entry ? entry.closeMode : "ack"; + if (closeMode === "no-ack") { + // Never reply, so the host close call times out and the host retires us. + return; + } + if (closeMode === "bad-ack") { + send({ jsonrpc: "2.0", id: message.id, result: { hostRouteId: "mismatched-route" } }); + return; + } + send({ jsonrpc: "2.0", id: message.id, result: { hostRouteId: params.hostRouteId } }); + return; + } + + if (method === "shutdown") { + send({ jsonrpc: "2.0", id: message.id, result: {} }); + setImmediate(() => process.exit(0)); + return; + } + + send({ + jsonrpc: "2.0", + id: message.id, + error: { code: -32601, message: `Unhandled method: ${method}` }, + }); +}); diff --git a/server/src/__tests__/heartbeat-pending-cleanup-sweep.test.ts b/server/src/__tests__/heartbeat-pending-cleanup-sweep.test.ts index f139d6e7f0..1a6babe409 100644 --- a/server/src/__tests__/heartbeat-pending-cleanup-sweep.test.ts +++ b/server/src/__tests__/heartbeat-pending-cleanup-sweep.test.ts @@ -49,6 +49,8 @@ if (!embeddedPostgresSupport.supported) { const ATTEMPTS_KEY = "pendingCleanupRetryAttempts"; const CAP_WARNED_KEY = "pendingCleanupRetryCapWarned"; const ATTEMPT_CAP = 5; +// The sweep reads at most this many oldest pending_cleanup rows per tick. +const SWEEP_PAGE_SIZE = 20; describeEmbeddedPostgres("heartbeat sweepPendingCleanupLeases", () => { let db!: ReturnType; @@ -98,7 +100,7 @@ describeEmbeddedPostgres("heartbeat sweepPendingCleanupLeases", () => { async function insertPendingCleanupLease(input: { companyId: string; - environmentId: string; + environmentId: string | null; updatedAt: Date; metadata?: Record; }): Promise { @@ -128,6 +130,34 @@ describeEmbeddedPostgres("heartbeat sweepPendingCleanupLeases", () => { return { destroyRunLease } as unknown as HeartbeatEnvironmentRuntime; } + // An orphan ephemeral lease that a failed acquire records. The sweep tears it + // down through retryPendingSandboxTeardown, not through destroyRunLease. + async function insertOrphanEphemeralLease(input: { + companyId: string; + environmentId: string | null; + updatedAt: Date; + metadata?: Record; + }): Promise { + const id = randomUUID(); + await db.insert(environmentLeases).values({ + id, + companyId: input.companyId, + environmentId: input.environmentId, + status: "pending_cleanup", + leasePolicy: "ephemeral", + provider: "fake", + providerLeaseId: `sandbox://fake/${id}`, + cleanupStatus: "failed", + metadata: { driver: "sandbox", provider: "fake", ...(input.metadata ?? {}) }, + acquiredAt: input.updatedAt, + lastUsedAt: input.updatedAt, + releasedAt: input.updatedAt, + createdAt: input.updatedAt, + updatedAt: input.updatedAt, + }); + return id; + } + it("test_pending_cleanup_sweep_retries_and_destroys_lease", async () => { const { companyId, environmentId } = await seedCompanyAndEnvironment(); const leaseId = await insertPendingCleanupLease({ @@ -301,6 +331,345 @@ describeEmbeddedPostgres("heartbeat sweepPendingCleanupLeases", () => { expect(metadata?.status).toBe("pending_cleanup"); }); + // Two unified sweeps can overlap on one orphan ephemeral lease. The master + // sweep is the single owner of the pending_cleanup rows. Its atomic per-attempt + // claim must let only one sweep tear the sandbox down, and the winning sweep + // must leave the lease in a final successful state. + it("test_concurrent_unified_sweeps_tear_an_orphan_down_once", async () => { + const { companyId, environmentId } = await seedCompanyAndEnvironment(); + const leaseId = await insertOrphanEphemeralLease({ + companyId, + environmentId, + updatedAt: new Date(Date.now() - 60 * 60 * 1000), + }); + + // The orphan teardown succeeds and does not release the lease; the sweep + // releases it. The counter records how many sweeps reached the teardown. + let teardownCount = 0; + const retryPendingSandboxTeardown = vi.fn(async () => { + teardownCount += 1; + }); + const runtime = { retryPendingSandboxTeardown } as unknown as HeartbeatEnvironmentRuntime; + + // Two sweeps run on two separate database clients, so they truly overlap. + // A single client serializes the queries on one connection and hides the + // race. The second client connects to the same embedded database. + const dbB = createDb(tempDb!.connectionString); + try { + // Warm up the second connection first, so the two sweeps start together. + await dbB.select({ id: environmentLeases.id }).from(environmentLeases).limit(1); + + const heartbeatA = heartbeatService(db, { environmentRuntime: runtime }); + const heartbeatB = heartbeatService(dbB, { environmentRuntime: runtime }); + + const backoffMs = 5 * 60 * 1000; + const [first, second] = await Promise.all([ + heartbeatA.sweepPendingCleanupLeases({ backoffMs }), + heartbeatB.sweepPendingCleanupLeases({ backoffMs }), + ]); + + // Only one sweep claimed the attempt and tore the sandbox down. + expect(teardownCount).toBe(1); + expect(first.destroyed + second.destroyed).toBe(1); + } finally { + await (dbB as unknown as { $client: { end: () => Promise } }).$client.end(); + } + + // The winning sweep left the lease in a final successful state and advanced + // the attempt count by exactly one. + const finalRow = await db + .select({ + status: environmentLeases.status, + cleanupStatus: environmentLeases.cleanupStatus, + metadata: environmentLeases.metadata, + }) + .from(environmentLeases) + .where(eq(environmentLeases.id, leaseId)) + .then((rows) => rows[0]); + expect(finalRow?.status).toBe("expired"); + expect(finalRow?.cleanupStatus).toBe("success"); + expect((finalRow?.metadata as Record | null)?.[ATTEMPTS_KEY]).toBe(1); + }); + + it("test_pending_cleanup_sweep_tears_down_reusable_lease_after_environment_delete", async () => { + const { companyId } = await seedCompanyAndEnvironment(); + // A reuse_by_environment lease whose environment a delete removed. The + // schema sets the environment reference to null and preserves the + // pending_cleanup row, so the lease still points at a live provider sandbox. + const leaseId = await insertPendingCleanupLease({ + companyId, + environmentId: null, + updatedAt: new Date(Date.now() - 60 * 60 * 1000), + }); + + // The environment row is gone, so the sweep must tear the sandbox down from + // the recorded lease data through retryPendingSandboxTeardown. It must not + // call destroyRunLease, which needs the environment. The teardown succeeds + // and does not release the lease; the sweep releases it. + const retryPendingSandboxTeardown = vi.fn(async (input: { environment: unknown }) => { + expect(input.environment).toBeNull(); + }); + const destroyRunLease = vi.fn(async () => null); + const runtime = { retryPendingSandboxTeardown, destroyRunLease } as unknown as HeartbeatEnvironmentRuntime; + const heartbeat = heartbeatService(db, { environmentRuntime: runtime }); + + const result = await heartbeat.sweepPendingCleanupLeases({ backoffMs: 0 }); + + expect(result).toEqual({ swept: 1, destroyed: 1, capped: 0 }); + expect(retryPendingSandboxTeardown).toHaveBeenCalledTimes(1); + expect(retryPendingSandboxTeardown.mock.calls[0]?.[0]).toMatchObject({ + lease: expect.objectContaining({ id: leaseId }), + }); + expect(destroyRunLease).not.toHaveBeenCalled(); + + const finalRow = await db + .select({ status: environmentLeases.status, cleanupStatus: environmentLeases.cleanupStatus }) + .from(environmentLeases) + .where(eq(environmentLeases.id, leaseId)) + .then((rows) => rows[0]); + expect(finalRow?.status).toBe("expired"); + expect(finalRow?.cleanupStatus).toBe("success"); + }); + + it("test_pending_cleanup_sweep_keeps_reusable_lease_when_recorded_teardown_fails", async () => { + const { companyId } = await seedCompanyAndEnvironment(); + const leaseId = await insertPendingCleanupLease({ + companyId, + environmentId: null, + updatedAt: new Date(Date.now() - 60 * 60 * 1000), + }); + + // The recorded-data teardown throws, so the sweep reverts the lease to + // pending_cleanup for a later retry. The claimed attempt still counts + // against the cap, so the retries stay bounded. + const retryPendingSandboxTeardown = vi.fn(async () => { + throw new Error("provider teardown failed"); + }); + const destroyRunLease = vi.fn(async () => null); + const runtime = { retryPendingSandboxTeardown, destroyRunLease } as unknown as HeartbeatEnvironmentRuntime; + const heartbeat = heartbeatService(db, { environmentRuntime: runtime }); + + const result = await heartbeat.sweepPendingCleanupLeases({ backoffMs: 0 }); + + expect(result).toEqual({ swept: 1, destroyed: 0, capped: 0 }); + expect(retryPendingSandboxTeardown).toHaveBeenCalledTimes(1); + expect(destroyRunLease).not.toHaveBeenCalled(); + + const finalRow = await db + .select({ + status: environmentLeases.status, + cleanupStatus: environmentLeases.cleanupStatus, + metadata: environmentLeases.metadata, + }) + .from(environmentLeases) + .where(eq(environmentLeases.id, leaseId)) + .then((rows) => rows[0]); + expect(finalRow?.status).toBe("pending_cleanup"); + expect(finalRow?.cleanupStatus).toBe("failed"); + expect((finalRow?.metadata as Record | null)?.[ATTEMPTS_KEY]).toBe(1); + }); + + it("test_pending_cleanup_sweep_skips_lease_while_plugin_worker_down_then_retries", async () => { + const { companyId } = await seedCompanyAndEnvironment(); + // An orphan ephemeral lease that a failed plugin acquire recorded. The sweep + // tears it down through retryPendingSandboxTeardown. + const leaseId = await insertOrphanEphemeralLease({ + companyId, + environmentId: null, + updatedAt: new Date(Date.now() - 60 * 60 * 1000), + }); + + // The plugin worker is down at first, then recovers on the second sweep. + let workerReady = false; + const isPendingCleanupWorkerReady = vi.fn(async () => workerReady); + const retryPendingSandboxTeardown = vi.fn(async () => {}); + const destroyRunLease = vi.fn(async () => null); + const runtime = { + isPendingCleanupWorkerReady, + retryPendingSandboxTeardown, + destroyRunLease, + } as unknown as HeartbeatEnvironmentRuntime; + const heartbeat = heartbeatService(db, { environmentRuntime: runtime }); + + // First sweep: the worker is down, so the sweep skips the lease. It never + // claims an attempt and never runs the teardown. + const first = await heartbeat.sweepPendingCleanupLeases({ backoffMs: 0 }); + expect(first).toEqual({ swept: 1, destroyed: 0, capped: 0 }); + expect(isPendingCleanupWorkerReady).toHaveBeenCalledTimes(1); + expect(retryPendingSandboxTeardown).not.toHaveBeenCalled(); + // The skipped lease consumed no finite attempt. + expect(await readAttempts(leaseId)).toBe(0); + const afterSkip = await db + .select({ status: environmentLeases.status }) + .from(environmentLeases) + .where(eq(environmentLeases.id, leaseId)) + .then((rows) => rows[0]?.status); + expect(afterSkip).toBe("pending_cleanup"); + + // Second sweep: the worker recovered, so the sweep claims an attempt and + // tears the sandbox down. + workerReady = true; + const second = await heartbeat.sweepPendingCleanupLeases({ backoffMs: 0 }); + expect(second).toEqual({ swept: 1, destroyed: 1, capped: 0 }); + expect(retryPendingSandboxTeardown).toHaveBeenCalledTimes(1); + + const finalRow = await db + .select({ status: environmentLeases.status, cleanupStatus: environmentLeases.cleanupStatus }) + .from(environmentLeases) + .where(eq(environmentLeases.id, leaseId)) + .then((rows) => rows[0]); + expect(finalRow?.status).toBe("expired"); + expect(finalRow?.cleanupStatus).toBe("success"); + }); + + it("test_pending_cleanup_sweep_never_caps_while_provider_unavailable_then_cleans_up", async () => { + const { companyId } = await seedCompanyAndEnvironment(); + // An orphan ephemeral lease that a failed plugin acquire recorded. The sweep + // tears it down through retryPendingSandboxTeardown. + const leaseId = await insertOrphanEphemeralLease({ + companyId, + environmentId: null, + updatedAt: new Date(Date.now() - 60 * 60 * 1000), + }); + + // The provider stays unavailable for more sweeps than the finite attempt + // cap. A long plugin reload or a long worker restart looks like this. The + // probe reports not ready every time, so no sweep claims an attempt. + let providerReady = false; + const isPendingCleanupWorkerReady = vi.fn(async () => providerReady); + const retryPendingSandboxTeardown = vi.fn(async () => {}); + const runtime = { + isPendingCleanupWorkerReady, + retryPendingSandboxTeardown, + } as unknown as HeartbeatEnvironmentRuntime; + const heartbeat = heartbeatService(db, { environmentRuntime: runtime }); + + // Run one more sweep than the cap. A per-sweep claim would exhaust the cap + // and strand the sandbox, so this proves the unavailable provider never + // burns an attempt. + for (let sweep = 0; sweep < ATTEMPT_CAP + 1; sweep += 1) { + const result = await heartbeat.sweepPendingCleanupLeases({ backoffMs: 0 }); + // The lease is swept and skipped every time; it never caps and never + // tears down while the provider is unavailable. + expect(result).toEqual({ swept: 1, destroyed: 0, capped: 0 }); + } + expect(retryPendingSandboxTeardown).not.toHaveBeenCalled(); + // The unavailable provider consumed no finite attempt across every sweep. + expect(await readAttempts(leaseId)).toBe(0); + const afterUnavailable = await db + .select({ status: environmentLeases.status }) + .from(environmentLeases) + .where(eq(environmentLeases.id, leaseId)) + .then((rows) => rows[0]?.status); + expect(afterUnavailable).toBe("pending_cleanup"); + + // The provider recovers, so the next sweep claims the first attempt and + // tears the sandbox down. + providerReady = true; + const recovered = await heartbeat.sweepPendingCleanupLeases({ backoffMs: 0 }); + expect(recovered).toEqual({ swept: 1, destroyed: 1, capped: 0 }); + expect(retryPendingSandboxTeardown).toHaveBeenCalledTimes(1); + + const finalRow = await db + .select({ status: environmentLeases.status, cleanupStatus: environmentLeases.cleanupStatus }) + .from(environmentLeases) + .where(eq(environmentLeases.id, leaseId)) + .then((rows) => rows[0]); + expect(finalRow?.status).toBe("expired"); + expect(finalRow?.cleanupStatus).toBe("success"); + }); + + it("test_pending_cleanup_sweep_does_not_let_unavailable_leases_starve_ready_leases", async () => { + const { companyId, environmentId } = await seedCompanyAndEnvironment(); + + // Fill one whole page with the oldest leases, and make every one of them + // unavailable. Their providers are not ready, so the sweep must not tear + // them down. + const unavailableIds = new Set(); + const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000); + for (let index = 0; index < SWEEP_PAGE_SIZE; index += 1) { + const id = await insertOrphanEphemeralLease({ + companyId, + environmentId: null, + updatedAt: twoHoursAgo, + }); + unavailableIds.add(id); + } + + // Add one newer lease with a ready provider. It sorts after the page of + // unavailable leases, so the first sweep never reaches it. + const readyLeaseId = await insertPendingCleanupLease({ + companyId, + environmentId, + updatedAt: new Date(Date.now() - 30 * 60 * 1000), + }); + + // The probe reports the ready lease's provider ready and every unavailable + // lease's provider not ready. + const isPendingCleanupWorkerReady = vi.fn( + async ({ lease }: { lease: { id: string } }) => !unavailableIds.has(lease.id), + ); + const retryPendingSandboxTeardown = vi.fn(async () => {}); + const destroyRunLease = vi.fn(async ({ lease }: { lease: { id: string } }) => { + const now = new Date(); + const row = await db + .update(environmentLeases) + .set({ status: "expired", cleanupStatus: "success", updatedAt: now }) + .where(eq(environmentLeases.id, lease.id)) + .returning() + .then((rows) => rows[0] ?? null); + return row ? { ...row, status: "expired" as const } : null; + }); + const runtime = { + isPendingCleanupWorkerReady, + retryPendingSandboxTeardown, + destroyRunLease, + } as unknown as HeartbeatEnvironmentRuntime; + const heartbeat = heartbeatService(db, { environmentRuntime: runtime }); + + // A five-minute backoff keeps every seeded lease eligible, because each one + // is older than the backoff. A deferred lease bumps its updatedAt to now, + // so the backoff then excludes it from the next page. + const backoffMs = 5 * 60 * 1000; + + // First sweep: the page holds only the unavailable leases. The sweep defers + // all of them and tears none down. The ready lease is not in this page. + const first = await heartbeat.sweepPendingCleanupLeases({ backoffMs }); + expect(first).toEqual({ swept: SWEEP_PAGE_SIZE, destroyed: 0, capped: 0 }); + expect(retryPendingSandboxTeardown).not.toHaveBeenCalled(); + expect(destroyRunLease).not.toHaveBeenCalled(); + // The deferred leases consumed no finite attempt. + for (const id of unavailableIds) { + expect(await readAttempts(id)).toBe(0); + } + + // Second sweep: the deferred unavailable leases now sit inside the backoff + // window, so the sweep skips them. The ready lease takes the page slot and + // tears down. This proves the unavailable leases never starve the ready + // lease. + const second = await heartbeat.sweepPendingCleanupLeases({ backoffMs }); + expect(second.destroyed).toBe(1); + expect(destroyRunLease).toHaveBeenCalledTimes(1); + expect(destroyRunLease.mock.calls[0]?.[0]).toMatchObject({ + lease: expect.objectContaining({ id: readyLeaseId }), + }); + + const readyStatus = await db + .select({ status: environmentLeases.status }) + .from(environmentLeases) + .where(eq(environmentLeases.id, readyLeaseId)) + .then((rows) => rows[0]?.status); + expect(readyStatus).toBe("expired"); + + // The unavailable leases stay in pending_cleanup for a later retry. + const stillPending = await db + .select({ id: environmentLeases.id }) + .from(environmentLeases) + .where(eq(environmentLeases.status, "pending_cleanup")) + .then((rows) => rows.map((row) => row.id)); + expect(new Set(stillPending)).toEqual(unavailableIds); + }); + // Read the current retry attempt count from a lease's metadata. async function readAttempts(leaseId: string): Promise { const metadata = await db @@ -938,4 +1307,40 @@ describeEmbeddedPostgres("heartbeat sweepPendingCleanupLeases", () => { const arrayMetadata = await readMetadata(arrayLeaseId); expect(arrayMetadata?.[ATTEMPTS_KEY]).toBe(1); }); + + it("flushes the in-process orphan buffer before the read, so a freshly landed row is swept the same tick", async () => { + const { companyId, environmentId } = await seedCompanyAndEnvironment(); + + // The flush lands one durable orphan row, exactly as the runtime buffer does + // after the database recovers. The sweep must run this flush before it reads + // the rows, so the same tick tears the freshly-landed orphan down. + const flushDeferredOrphanCleanups = vi.fn(async () => { + await insertOrphanEphemeralLease({ + companyId, + environmentId, + updatedAt: new Date(Date.now() - 60 * 60 * 1000), + }); + return { recovered: 1, pending: 0 }; + }); + // The recorded-data teardown succeeds, so the sweep releases the lease. + const retryPendingSandboxTeardown = vi.fn(async () => {}); + const runtime = { + flushDeferredOrphanCleanups, + retryPendingSandboxTeardown, + } as unknown as HeartbeatEnvironmentRuntime; + const heartbeat = heartbeatService(db, { environmentRuntime: runtime }); + + const result = await heartbeat.sweepPendingCleanupLeases({ backoffMs: 0 }); + + // The flush ran once before the read, so the row it landed is visible to the + // same sweep and tears down through the recorded-data teardown path. + expect(flushDeferredOrphanCleanups).toHaveBeenCalledTimes(1); + expect(retryPendingSandboxTeardown).toHaveBeenCalledTimes(1); + expect(result).toEqual({ swept: 1, destroyed: 1, capped: 0 }); + + const rows = await db.select().from(environmentLeases); + expect(rows).toHaveLength(1); + expect(rows[0]?.status).toBe("expired"); + expect(rows[0]?.cleanupStatus).toBe("success"); + }); }); diff --git a/server/src/__tests__/openapi-routes.test.ts b/server/src/__tests__/openapi-routes.test.ts index ce421a6c8c..2d1eeabfdc 100644 --- a/server/src/__tests__/openapi-routes.test.ts +++ b/server/src/__tests__/openapi-routes.test.ts @@ -74,6 +74,12 @@ const explicitOpenApiCoverageExclusions = new Set([ "smoke-lab.ts", ]); +// The set of contract-first routes whose OpenAPI document leads the mounted +// request handler. The company-and-environment Claude setup-token login routes +// now have request handlers, so the set is empty. A new contract-first route +// belongs here only until its handler lands. +const specOnlyContractFirstRoutes = new Set([]); + function createApp() { const app = express(); app.use("/api", openApiRoutes()); @@ -253,7 +259,9 @@ describe("openapi routes", () => { const { routes: specRoutes } = loadSpecRoutes(); const missingInSpec = [...actualRoutes].filter((route) => !specRoutes.has(route)).sort(); - const extraInSpec = [...specRoutes].filter((route) => !actualRoutes.has(route)).sort(); + const extraInSpec = [...specRoutes] + .filter((route) => !actualRoutes.has(route) && !specOnlyContractFirstRoutes.has(route)) + .sort(); expect({ unknownRouteFiles, missingInSpec, extraInSpec }).toEqual({ unknownRouteFiles: [], @@ -288,4 +296,74 @@ describe("openapi routes", () => { expect(spec.paths["/api/board-api-keys"].post.responses["201"]).toBeDefined(); expect(spec.paths["/api/companies/import"].post.responses["202"]).toBeDefined(); }); + + it("publishes the Claude browser-code grammar and strict setup-token response shapes", () => { + const { spec } = loadSpecRoutes(); + const base = "/api/companies/{companyId}/setup-token-login-sessions"; + + // The submitted browser code carries the bounded printable-ASCII grammar. + const codeBody = + spec.paths[`${base}/{sessionId}/code`].post.requestBody.content["application/json"].schema; + const browserCode = codeBody.properties.browserCode; + expect(browserCode.minLength).toBe(1); + expect(browserCode.maxLength).toBe(512); + expect(typeof browserCode.pattern).toBe("string"); + expect(browserCode.pattern.length).toBeGreaterThan(0); + + // Every Claude request object forbids an unknown property. + const startBody = + spec.paths[base].post.requestBody.content["application/json"].schema; + expect(startBody.additionalProperties).toBe(false); + expect(codeBody.additionalProperties).toBe(false); + + // The four contract-first routes carry typed strict response schemas. + const responseSchemas: Record> = { + start: spec.paths[base].post.responses["201"].content["application/json"].schema, + status: spec.paths[`${base}/{sessionId}`].get.responses["200"].content["application/json"].schema, + prompt: spec.paths[`${base}/{sessionId}/prompt`].get.responses["200"].content["application/json"].schema, + code: spec.paths[`${base}/{sessionId}/code`].post.responses["200"].content["application/json"].schema, + }; + const forbiddenProperties = ["token", "accountId", "leaseId"]; + for (const [name, schema] of Object.entries(responseSchemas)) { + expect(schema.type, `${name} response is a typed object`).toBe("object"); + expect(schema.additionalProperties, `${name} response is strict`).toBe(false); + const properties = (schema.properties ?? {}) as Record; + expect(Object.keys(properties).length, `${name} response lists properties`).toBeGreaterThan(0); + for (const forbidden of forbiddenProperties) { + expect(properties[forbidden], `${name} response hides ${forbidden}`).toBeUndefined(); + } + // No property name looks like a raw prompt secret or a token. + for (const property of Object.keys(properties)) { + expect(/token|secret|accountId|leaseId/i.test(property), `${name}.${property} is not secret-adjacent`).toBe( + false, + ); + } + } + + // The status and code routes share the public response; it hides the prompt. + expect(responseSchemas.status.properties).toEqual(responseSchemas.code.properties); + expect((responseSchemas.status.properties as Record).prompt).toBeUndefined(); + // The owner start response adds the panel mode and the one-time prompt. + expect((responseSchemas.start.properties as Record).panelMode).toBeDefined(); + expect((responseSchemas.start.properties as Record).prompt).toBeDefined(); + // The prompt route returns the authorization URL and the optional transport + // advisory. The advisory is present on a non-confidential transport, so the + // client can show a non-blocking disclaimer. + expect(Object.keys(responseSchemas.prompt.properties as Record)).toEqual([ + "authorizationUrl", + "transportAdvisory", + ]); + }); + + it("documents the 404 non-member gate on the Claude setup-token cancel route", () => { + const { spec } = loadSpecRoutes(); + const cancel = + spec.paths["/api/companies/{companyId}/setup-token-login-sessions/{sessionId}/cancel"].post; + // The 404 is reachable at run time. The company-access gate returns a fixed + // 404 for a non-member before the cancel logic runs, so the spec declares + // it. The idempotent cancel still returns 200 for an owner-scoped missing, + // terminal, or foreign session id. + const codes = Object.keys(cancel.responses).sort(); + expect(codes).toEqual(["200", "401", "403", "404"]); + }); }); diff --git a/server/src/__tests__/plugin-worker-manager.test.ts b/server/src/__tests__/plugin-worker-manager.test.ts index 7d837a47dd..72fb0e5d08 100644 --- a/server/src/__tests__/plugin-worker-manager.test.ts +++ b/server/src/__tests__/plugin-worker-manager.test.ts @@ -9,6 +9,7 @@ import { type HostServices, type HostToWorkerMethods, } from "@paperclipai/plugin-sdk"; +import { CLAUDE_SETUP_TOKEN_COMMAND } from "@paperclipai/adapter-claude-local/server"; import { appendStderrExcerpt, createPluginWorkerHandle, @@ -1054,3 +1055,262 @@ describe("plugin worker manager execute.log route", () => { } }); }); + +// --------------------------------------------------------------------------- +// Host-owned setup-token login pseudo-terminal route gate +// --------------------------------------------------------------------------- + +const SETUP_TOKEN_PTY_WORKER_ENTRYPOINT = path.join( + FIXTURES_DIR, + "plugin-worker-setup-token-pty.cjs", +); + +function makeSetupTokenPtyHandle(extra?: Record) { + return createPluginWorkerHandle("test.plugin", { + entrypointPath: SETUP_TOKEN_PTY_WORKER_ENTRYPOINT, + manifest: TEST_MANIFEST, + config: {}, + instanceInfo: { instanceId: "instance-1", hostVersion: "1.0.0" }, + apiVersion: 1, + hostHandlers: {}, + ...extra, + }); +} + +function ptyOpenInput(directive: unknown) { + return { + driverKey: "daytona", + companyId: "company-1", + environmentId: "env-1", + // The test directive rides in `providerLeaseId`, an opaque field the manager + // forwards to the worker unchanged. The manager allowlists `command`, so the + // command stays the fixed `CLAUDE_SETUP_TOKEN_COMMAND` for every route-gate + // case. + providerLeaseId: JSON.stringify(directive), + command: CLAUDE_SETUP_TOKEN_COMMAND, + }; +} + +describe("plugin worker manager setup-token pty route gate", () => { + it("rejects a command that is not the allowlisted setup-token command before the worker call", async () => { + const handle = makeSetupTokenPtyHandle(); + try { + await handle.start(); + // A caller passes a command other than the fixed `CLAUDE_SETUP_TOKEN_COMMAND`. + // The manager rejects it with one fixed non-secret error before the worker + // call, so no arbitrary process spawns in the sandbox pseudo-terminal. + await expect( + handle.openSetupTokenPtySession({ + driverKey: "daytona", + companyId: "company-1", + environmentId: "env-1", + providerLeaseId: JSON.stringify({ mode: "normal" }), + command: "rm -rf /", + }), + ).rejects.toThrow("SETUP_TOKEN_PTY_COMMAND_NOT_ALLOWED"); + // The rejected open never consumed the single route, so a later open with + // the allowlisted command still succeeds. + const session = await handle.openSetupTokenPtySession( + ptyOpenInput({ mode: "normal" }), + ); + expect(session).toBeDefined(); + await session.close(); + } finally { + await handle.stop().catch(() => undefined); + } + }); + + it("permits one active credential pseudo-terminal per worker", async () => { + const handle = makeSetupTokenPtyHandle(); + try { + await handle.start(); + const first = await handle.openSetupTokenPtySession( + ptyOpenInput({ mode: "normal" }), + ); + // A second open while the first route is not closed rejects with one fixed + // non-secret error before it reaches the worker. + await expect( + handle.openSetupTokenPtySession(ptyOpenInput({ mode: "normal" })), + ).rejects.toThrow("SETUP_TOKEN_PTY_ROUTE_BUSY"); + await first.close(); + // After the first route closes and the worker acknowledges the close, a new + // open is admitted. + const second = await handle.openSetupTokenPtySession( + ptyOpenInput({ mode: "normal" }), + ); + await second.close(); + } finally { + await handle.stop().catch(() => undefined); + } + }); + + it("delivers output only for the exact bound worker session id and drops a mismatch", async () => { + const handle = makeSetupTokenPtyHandle(); + try { + await handle.start(); + const session = await handle.openSetupTokenPtySession( + ptyOpenInput({ + workerSessionId: "ws-A", + outputs: [ + { chunk: "good-1" }, + { chunk: "forged", sid: "ws-EVIL" }, + { chunk: "good-2" }, + ], + exitCode: 0, + }), + ); + const chunks: string[] = []; + session.onData((chunk) => chunks.push(chunk)); + await expect(session.wait()).resolves.toEqual({ exitCode: 0 }); + // The forged notification carries a wrong worker session id, so the host + // drops it. Only the two bound chunks reach the listener, in order. + expect(chunks).toEqual(["good-1", "good-2"]); + await session.close(); + } finally { + await handle.stop().catch(() => undefined); + } + }); + + it("routes delayed input to the worker and back to the listener", async () => { + const handle = makeSetupTokenPtyHandle(); + try { + await handle.start(); + const session = await handle.openSetupTokenPtySession( + ptyOpenInput({ workerSessionId: "ws-A" }), + ); + const chunks: string[] = []; + session.onData((chunk) => chunks.push(chunk)); + session.write("browser-code"); + // The worker echoes the input as one output notification for the bound + // session, so the listener receives it. + await vi.waitFor(() => expect(chunks).toContain("echo:browser-code")); + await session.close(); + } finally { + await handle.stop().catch(() => undefined); + } + }); + + it("terminalizes the route when the cumulative output passes the per-route bound", async () => { + const handle = makeSetupTokenPtyHandle({ + setupTokenPtyLimits: { maxTotalChars: 10 }, + }); + try { + await handle.start(); + const session = await handle.openSetupTokenPtySession( + ptyOpenInput({ + outputs: [ + { chunk: "aaaaa" }, // total 5 → delivered + { chunk: "bbbbb" }, // total 10 → delivered + { chunk: "ccccc" }, // total 15 > 10 → terminalize + ], + }), + ); + const chunks: string[] = []; + session.onData((chunk) => chunks.push(chunk)); + // The per-route bound terminalizes the route, so the login wait resolves + // with a null exit code and the third chunk never reaches the listener. + await expect(session.wait()).resolves.toEqual({ exitCode: null }); + expect(chunks).toEqual(["aaaaa", "bbbbb"]); + } finally { + await handle.stop().catch(() => undefined); + } + }); + + it("terminalizes and fails closed on a malformed open reply, then admits a later open", async () => { + const handle = makeSetupTokenPtyHandle(); + try { + await handle.start(); + await expect( + handle.openSetupTokenPtySession(ptyOpenInput({ mode: "malformed-open" })), + ).rejects.toThrow("SETUP_TOKEN_PTY_OPEN_FAILED"); + // The terminalize closed the route by the host route id and the worker + // acknowledged the close, so a later open is admitted. + const session = await handle.openSetupTokenPtySession( + ptyOpenInput({ mode: "normal" }), + ); + expect(session).toBeDefined(); + await session.close(); + } finally { + await handle.stop().catch(() => undefined); + } + }); + + it("terminalizes the route on an open timeout", async () => { + const handle = makeSetupTokenPtyHandle({ + setupTokenPtyLimits: { openTimeoutMs: 200 }, + }); + try { + await handle.start(); + await expect( + handle.openSetupTokenPtySession(ptyOpenInput({ mode: "no-open-reply" })), + ).rejects.toThrow(); + } finally { + await handle.stop().catch(() => undefined); + } + }); + + it("binds the worker session id one time and ignores a duplicate open reply", async () => { + const handle = makeSetupTokenPtyHandle(); + try { + await handle.start(); + const session = await handle.openSetupTokenPtySession( + ptyOpenInput({ + mode: "duplicate-open-reply", + workerSessionId: "ws-A", + outputs: [{ chunk: "hello" }], + exitCode: 0, + }), + ); + const chunks: string[] = []; + session.onData((chunk) => chunks.push(chunk)); + // The duplicate open reply never rebinds or reopens the route, so the + // session runs normally on the one bind. + await expect(session.wait()).resolves.toEqual({ exitCode: 0 }); + expect(chunks).toEqual(["hello"]); + await session.close(); + } finally { + await handle.stop().catch(() => undefined); + } + }); + + it("closes the route with a fixed exit when the worker exits", async () => { + const handle = makeSetupTokenPtyHandle(); + try { + await handle.start(); + const session = await handle.openSetupTokenPtySession( + ptyOpenInput({ mode: "normal" }), + ); + const waitResult = session.wait(); + await handle.stop(); + // A worker exit closes the one route and resolves the login wait with the + // fixed non-secret exit. + await expect(waitResult).resolves.toEqual({ exitCode: null }); + } finally { + await handle.stop().catch(() => undefined); + } + }); + + it("retires the worker on an unconfirmed close acknowledgement", async () => { + const handle = makeSetupTokenPtyHandle({ + setupTokenPtyLimits: { closeTimeoutMs: 200 }, + }); + try { + await handle.start(); + const exited = new Promise((resolve) => { + handle.on("exit", () => resolve()); + }); + const session = await handle.openSetupTokenPtySession( + ptyOpenInput({ mode: "normal", closeMode: "bad-ack" }), + ); + await session.close(); + // The close acknowledgement carried a mismatched host route id, so the host + // fails closed and retires the worker before any reuse. + await exited; + await expect( + handle.openSetupTokenPtySession(ptyOpenInput({ mode: "normal" })), + ).rejects.toThrow(); + } finally { + await handle.stop().catch(() => undefined); + } + }); +}); diff --git a/server/src/__tests__/sandbox-orphan-cleanup-spool.test.ts b/server/src/__tests__/sandbox-orphan-cleanup-spool.test.ts new file mode 100644 index 0000000000..74b984114b --- /dev/null +++ b/server/src/__tests__/sandbox-orphan-cleanup-spool.test.ts @@ -0,0 +1,93 @@ +import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + createSandboxOrphanCleanupSpool, + type DeferredOrphanCleanupRecord, +} from "../services/sandbox-orphan-cleanup-spool.ts"; + +function buildRecord(overrides: Partial = {}): DeferredOrphanCleanupRecord { + return { + companyId: "company-1", + environmentId: "environment-1", + executionWorkspaceId: null, + issueId: null, + heartbeatRunId: "run-1", + provider: "fake", + providerLeaseId: "sandbox://fake/lease-1", + metadata: { driver: "sandbox", pluginKey: "example" }, + ...overrides, + }; +} + +describe("createSandboxOrphanCleanupSpool", () => { + let dir!: string; + + beforeEach(async () => { + dir = await mkdtemp(path.join(os.tmpdir(), "orphan-spool-unit-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }).catch(() => undefined); + }); + + it("persists a record and loads it back after a restart", async () => { + const writer = createSandboxOrphanCleanupSpool(dir); + const record = buildRecord(); + expect(await writer.append(record)).toBe(true); + + // A fresh spool over the same directory models a process restart. The record + // survives, so the load returns it with every field intact. + const reader = createSandboxOrphanCleanupSpool(dir); + const loaded = await reader.load(); + expect(loaded).toHaveLength(1); + expect(loaded[0]).toEqual(record); + }); + + it("removes a record so a later load returns nothing", async () => { + const spool = createSandboxOrphanCleanupSpool(dir); + const record = buildRecord(); + await spool.append(record); + await spool.remove(record); + expect(await spool.load()).toEqual([]); + expect((await readdir(dir)).filter((name) => name.endsWith(".json"))).toHaveLength(0); + }); + + it("keeps one file when the same orphan is appended twice", async () => { + const spool = createSandboxOrphanCleanupSpool(dir); + const record = buildRecord(); + await spool.append(record); + await spool.append(record); + // The file name keys on the provider lease id, so the second append overwrites + // the first file instead of persisting a duplicate. + const loaded = await spool.load(); + expect(loaded).toHaveLength(1); + }); + + it("persists a record with a null provider lease id", async () => { + const spool = createSandboxOrphanCleanupSpool(dir); + const record = buildRecord({ providerLeaseId: null }); + expect(await spool.append(record)).toBe(true); + const loaded = await spool.load(); + expect(loaded).toHaveLength(1); + expect(loaded[0]?.providerLeaseId).toBeNull(); + }); + + it("skips a malformed spool file and returns the other records", async () => { + const spool = createSandboxOrphanCleanupSpool(dir); + await spool.append(buildRecord()); + // A torn or hand-edited file cannot authorize a teardown, so the load skips it + // and keeps the valid record. + await writeFile(path.join(dir, "broken.json"), "{ not json", "utf8"); + await writeFile(path.join(dir, "wrong-shape.json"), JSON.stringify({ companyId: 1 }), "utf8"); + const loaded = await spool.load(); + expect(loaded).toHaveLength(1); + expect(loaded[0]?.providerLeaseId).toBe("sandbox://fake/lease-1"); + }); + + it("returns an empty list when the spool directory does not exist", async () => { + const spool = createSandboxOrphanCleanupSpool(path.join(dir, "missing")); + expect(await spool.load()).toEqual([]); + }); +}); diff --git a/server/src/__tests__/secrets-routes-claude-oauth-service.test.ts b/server/src/__tests__/secrets-routes-claude-oauth-service.test.ts new file mode 100644 index 0000000000..8724ea1181 --- /dev/null +++ b/server/src/__tests__/secrets-routes-claude-oauth-service.test.ts @@ -0,0 +1,610 @@ +// The narrow Claude Code OAuth secret helper and the owner-bound compare-and-set +// live in the secret service, not in a route. These tests exercise the real +// service against an embedded Postgres database, so the partial unique index, +// the expected-version predicate, and the session-id idempotency all run against +// real constraints. The route-level tests stay in secrets-routes.test.ts, which +// mocks the service and cannot exercise the database behavior. + +import { randomUUID } from "node:crypto"; +import { mkdirSync, rmSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { + activityLog, + companies, + companyMemberships, + companySecretBindings, + companySecretVersions, + companySecrets, + createDb, + secretAccessEvents, + userSecretDeclarations, + userSecretDefinitions, +} from "@paperclipai/db"; +import { and, eq } from "drizzle-orm"; +import { HttpError } from "../errors.js"; +import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js"; +import { secretService } from "../services/secrets.js"; + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +if (!embeddedPostgresSupport.supported) { + console.warn( + `Skipping Claude OAuth secret service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, + ); +} + +// The fixed key the narrow helper owns. A caller never selects it. +const CLAUDE_KEY = "CLAUDE_CODE_OAUTH_TOKEN"; +const CLAUDE_NAME = "Claude Code OAuth token"; + +describeEmbeddedPostgres("secretService Claude Code OAuth helper and compare-and-set", () => { + let stopDb: (() => Promise) | null = null; + let db!: ReturnType; + const previousKeyFile = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; + const secretsTmpDir = path.join(os.tmpdir(), `paperclip-claude-oauth-${randomUUID()}`); + + beforeAll(async () => { + mkdirSync(secretsTmpDir, { recursive: true }); + process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = path.join(secretsTmpDir, "master.key"); + const started = await startEmbeddedPostgresTestDatabase("claude-oauth-secrets"); + stopDb = started.cleanup; + db = createDb(started.connectionString); + }); + + afterEach(async () => { + await db.delete(activityLog); + await db.delete(secretAccessEvents); + await db.delete(userSecretDeclarations); + await db.delete(companySecretBindings); + await db.delete(companySecretVersions); + await db.delete(companySecrets); + await db.delete(userSecretDefinitions); + await db.delete(companyMemberships); + await db.delete(companies); + }); + + afterAll(async () => { + if (stopDb) await stopDb(); + if (previousKeyFile === undefined) { + delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; + } else { + process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = previousKeyFile; + } + rmSync(secretsTmpDir, { recursive: true, force: true }); + }); + + async function seedCompany(name = "Acme") { + const companyId = randomUUID(); + await db.insert(companies).values({ + id: companyId, + name, + issuePrefix: `T${companyId.slice(0, 7)}`.toUpperCase(), + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + return companyId; + } + + async function seedCompanyMember(companyId: string, userId: string) { + await db.insert(companyMemberships).values({ + companyId, + principalType: "user", + principalId: userId, + status: "active", + membershipRole: "owner", + createdAt: new Date(), + updatedAt: new Date(), + }); + } + + async function countVersions(secretId: string) { + const rows = await db + .select() + .from(companySecretVersions) + .where(eq(companySecretVersions.secretId, secretId)); + return rows.length; + } + + // --- the narrow secret-definition helper ------------------------ + + it("creates the fixed Claude OAuth definition with the compile-time key and fixed properties", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const definition = await svc.ensureClaudeOAuthUserSecretDefinition(companyId, { userId: "user-1", agentId: null }); + + expect(definition.key).toBe(CLAUDE_KEY); + expect(definition.name).toBe(CLAUDE_NAME); + expect(definition.provider).toBe("local_encrypted"); + expect(definition.managedMode).toBe("paperclip_managed"); + expect(definition.status).toBe("active"); + }); + + it("reuses an exact compatible definition without a second row", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const first = await svc.ensureClaudeOAuthUserSecretDefinition(companyId); + const second = await svc.ensureClaudeOAuthUserSecretDefinition(companyId); + + expect(second.id).toBe(first.id); + const rows = await db + .select() + .from(userSecretDefinitions) + .where(eq(userSecretDefinitions.companyId, companyId)); + expect(rows).toHaveLength(1); + }); + + it("rejects a conflicting definition with 409 and does not mutate it", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + // A pre-existing definition holds the fixed key but a different name. It is + // not the exact fixed shape, so the helper must reject it and leave it alone. + const conflicting = await svc.createUserSecretDefinition(companyId, { + key: CLAUDE_KEY, + name: "Some other Claude token", + provider: "local_encrypted", + }); + + await expect(svc.ensureClaudeOAuthUserSecretDefinition(companyId)).rejects.toMatchObject({ status: 409 }); + + const row = await db + .select() + .from(userSecretDefinitions) + .where(eq(userSecretDefinitions.id, conflicting.id)) + .then((rows) => rows[0]); + expect(row?.name).toBe("Some other Claude token"); + }); + + // --- the owner-bound compare-and-set ---------------------------- + + it("first_write creates the owner value and stores the session id", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const result = await svc.completeClaudeOAuthUserSecret( + companyId, + "user-1", + { sessionId: "session-a", mode: "first_write", value: "oauth-token-value" }, + { userId: "user-1", agentId: null }, + ); + + expect(result.secretId).toBeTruthy(); + expect(result.latestVersion).toBe(1); + expect(await countVersions(result.secretId)).toBe(1); + }); + + it("first_write conflicts with 409 when a different session already wrote the owner value", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + + await expect( + svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "first_write", + value: "another-token-value", + }), + ).rejects.toMatchObject({ status: 409 }); + }); + + it("first_write is idempotent for the same session id and creates no new version", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const first = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + const repeat = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + + expect(repeat.secretId).toBe(first.secretId); + expect(repeat.latestVersion).toBe(1); + expect(await countVersions(first.secretId)).toBe(1); + }); + + it("confirmed_rotation requires expectedSecretId and expectedLatestVersion", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + + await expect( + svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-value", + }), + ).rejects.toMatchObject({ status: 422 }); + }); + + it("confirmed_rotation rotates to the next version when the expected version matches", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const first = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + + const rotated = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-value", + expectedSecretId: first.secretId, + expectedLatestVersion: first.latestVersion, + }); + + expect(rotated.secretId).toBe(first.secretId); + expect(rotated.latestVersion).toBe(2); + expect(await countVersions(first.secretId)).toBe(2); + }); + + it("confirmed_rotation returns a fixed stale-confirmation 409 for a wrong expected version", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const first = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + // A first confirmed rotation moves the value to version 2. + await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-value", + expectedSecretId: first.secretId, + expectedLatestVersion: 1, + }); + + // A later confirmation still carries the stale version 1 and must conflict. + await expect( + svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-c", + mode: "confirmed_rotation", + value: "third-token-value", + expectedSecretId: first.secretId, + expectedLatestVersion: 1, + }), + ).rejects.toMatchObject({ status: 409 }); + + // The stale confirmation created no new version. + expect(await countVersions(first.secretId)).toBe(2); + }); + + it("confirmed_rotation is idempotent for the same session id and creates no new version", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const first = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + const rotated = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-value", + expectedSecretId: first.secretId, + expectedLatestVersion: 1, + }); + + const repeat = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-value", + expectedSecretId: first.secretId, + expectedLatestVersion: 1, + }); + + expect(repeat.secretId).toBe(rotated.secretId); + expect(repeat.latestVersion).toBe(2); + expect(await countVersions(first.secretId)).toBe(2); + }); + + it("two concurrent confirmed rotations return one fixed stale 409 and add one version", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const first = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + expect(first.latestVersion).toBe(1); + + // Both confirmations read the same latest version 1 and both target the next + // version 2. The unique index on (secretId, version) lets one insert win. The + // loser must return the same fixed stale 409, not a raw database error. + const [a, b] = await Promise.allSettled([ + svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-b", + expectedSecretId: first.secretId, + expectedLatestVersion: 1, + }), + svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-c", + mode: "confirmed_rotation", + value: "rotated-token-c", + expectedSecretId: first.secretId, + expectedLatestVersion: 1, + }), + ]); + + const fulfilled = [a, b].filter((r) => r.status === "fulfilled"); + const rejected = [a, b].filter((r) => r.status === "rejected"); + expect(fulfilled).toHaveLength(1); + expect(rejected).toHaveLength(1); + + // The loser returns the one fixed stale-confirmation 409. The text is the + // same for every stale race, so it discloses no owner-value state. + const loser = (rejected[0] as PromiseRejectedResult).reason; + expect(loser).toBeInstanceOf(HttpError); + expect((loser as HttpError).status).toBe(409); + expect((loser as HttpError).message).toContain("The Claude login confirmation is stale"); + + const winner = fulfilled[0] as PromiseFulfilledResult<{ latestVersion: number }>; + expect(winner.value.latestVersion).toBe(2); + + // Only the two winning versions remain. The loser left no extra version row. + expect(await countVersions(first.secretId)).toBe(2); + }); + + it("normalizes a next-version unique collision to the fixed stale 409 and adds no row", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const first = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + expect(first.latestVersion).toBe(1); + + // Reproduce the exact race window. A concurrent winner inserted the next + // version row but has not yet committed its owner-bound compare-and-set, so + // the latest version still reads 1. A second confirmation that carries the + // same expected version 1 passes the pre-check, then hits the (secretId, + // version) unique index on its own insert. That one collision must return the + // fixed stale 409, not a raw database error. + await db.insert(companySecretVersions).values({ + secretId: first.secretId, + version: 2, + material: {}, + valueSha256: "placeholder-sha256", + fingerprintSha256: "placeholder-sha256", + status: "disabled", + }); + expect(await countVersions(first.secretId)).toBe(2); + + let caught: unknown; + try { + await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-value", + expectedSecretId: first.secretId, + expectedLatestVersion: 1, + }); + } catch (error) { + caught = error; + } + + expect(caught).toBeInstanceOf(HttpError); + expect((caught as HttpError).status).toBe(409); + expect((caught as HttpError).message).toContain("The Claude login confirmation is stale"); + + // The failed insert added no new row and did not advance the latest version. + expect(await countVersions(first.secretId)).toBe(2); + const row = await db + .select() + .from(companySecrets) + .where(eq(companySecrets.id, first.secretId)) + .then((rows) => rows[0]); + expect(row?.latestVersion).toBe(1); + }); + + it("two concurrent first writes leave exactly one owner value through the partial unique index", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const [a, b] = await Promise.allSettled([ + svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "token-a", + }), + svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "first_write", + value: "token-b", + }), + ]); + + const fulfilled = [a, b].filter((r) => r.status === "fulfilled"); + const rejected = [a, b].filter((r) => r.status === "rejected"); + expect(fulfilled).toHaveLength(1); + expect(rejected).toHaveLength(1); + + const values = await db + .select() + .from(companySecrets) + .where(and(eq(companySecrets.companyId, companyId), eq(companySecrets.scope, "user"))); + expect(values).toHaveLength(1); + }); + + it("confirmed_rotation on a value from another owner returns not-found", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + await seedCompanyMember(companyId, "user-2"); + const svc = secretService(db); + + const owned = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + + // user-2 points at user-1's secret id. The owner-scoped lookup fails closed. + await expect( + svc.completeClaudeOAuthUserSecret(companyId, "user-2", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-value", + expectedSecretId: owned.secretId, + expectedLatestVersion: 1, + }), + ).rejects.toMatchObject({ status: 404 }); + }); + + it("confirmed_rotation on a value from another company returns not-found", async () => { + const companyA = await seedCompany("Acme"); + const companyB = await seedCompany("Globex"); + await seedCompanyMember(companyA, "user-1"); + await seedCompanyMember(companyB, "user-1"); + const svc = secretService(db); + + const owned = await svc.completeClaudeOAuthUserSecret(companyA, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + + await expect( + svc.completeClaudeOAuthUserSecret(companyB, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-value", + expectedSecretId: owned.secretId, + expectedLatestVersion: 1, + }), + ).rejects.toBeInstanceOf(HttpError); + }); + + it("readClaudeOAuthUserSecretStatus returns the secret id and the version for the owner value", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const first = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + const rotated = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-token-value", + expectedSecretId: first.secretId, + expectedLatestVersion: 1, + }); + + const status = await svc.readClaudeOAuthUserSecretStatus(companyId, "user-1"); + expect(status).toEqual({ secretId: first.secretId, latestVersion: rotated.latestVersion }); + // The status carries no token value. + expect(JSON.stringify(status)).not.toContain("oauth-token-value"); + expect(JSON.stringify(status)).not.toContain("rotated-token-value"); + }); + + it("readClaudeOAuthUserSecretStatus returns null when the owner has no value", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + await seedCompanyMember(companyId, "user-2"); + const svc = secretService(db); + + // user-1 has a value; user-2 does not. The reader is owner-scoped, so it + // returns null for user-2 and the metadata for user-1. + await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + + expect(await svc.readClaudeOAuthUserSecretStatus(companyId, "user-2")).toBeNull(); + // No definition-only or empty company also returns null (the reader never + // creates the definition). + const emptyCompany = await seedCompany("Globex"); + await seedCompanyMember(emptyCompany, "user-1"); + expect(await svc.readClaudeOAuthUserSecretStatus(emptyCompany, "user-1")).toBeNull(); + }); + + it("readClaudeOAuthUserSecretStatus is owner-scoped and company-scoped", async () => { + const companyA = await seedCompany("Acme"); + const companyB = await seedCompany("Globex"); + await seedCompanyMember(companyA, "user-1"); + await seedCompanyMember(companyB, "user-1"); + const svc = secretService(db); + + await svc.completeClaudeOAuthUserSecret(companyA, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: "oauth-token-value", + }); + + // The same user in another company has no value there. + expect(await svc.readClaudeOAuthUserSecretStatus(companyB, "user-1")).toBeNull(); + // Another owner in the same company has no value. + await seedCompanyMember(companyA, "user-2"); + expect(await svc.readClaudeOAuthUserSecretStatus(companyA, "user-2")).toBeNull(); + }); + + it("keeps the token out of every activity detail", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + const token = "super-secret-oauth-token"; + + const first = await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-a", + mode: "first_write", + value: token, + }); + await svc.completeClaudeOAuthUserSecret(companyId, "user-1", { + sessionId: "session-b", + mode: "confirmed_rotation", + value: "rotated-secret-oauth-token", + expectedSecretId: first.secretId, + expectedLatestVersion: 1, + }); + + const activity = await db.select().from(activityLog); + expect(JSON.stringify(activity)).not.toContain(token); + expect(JSON.stringify(activity)).not.toContain("rotated-secret-oauth-token"); + }); +}); diff --git a/server/src/__tests__/secrets-service-dangling-user-secret.test.ts b/server/src/__tests__/secrets-service-dangling-user-secret.test.ts new file mode 100644 index 0000000000..ed4b311bfc --- /dev/null +++ b/server/src/__tests__/secrets-service-dangling-user-secret.test.ts @@ -0,0 +1,190 @@ +import { randomUUID } from "node:crypto"; +import { mkdirSync, rmSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { + activityLog, + companies, + companyMemberships, + companySecretBindings, + companySecretVersions, + companySecrets, + createDb, + secretAccessEvents, + userSecretDeclarations, + userSecretDefinitions, +} from "@paperclipai/db"; +import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js"; +import { secretService } from "../services/secrets.js"; + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +if (!embeddedPostgresSupport.supported) { + console.warn( + `Skipping dangling user-secret tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, + ); +} + +// A user can delete a user-secret definition while an agent config still +// references it. Later, a direct-resolution path (Test or save) resolves that +// dangling `user_secret_ref` binding and fails. These tests assert the failure +// names the environment variable, the agent id, and the unresolved definition +// key, and that it never leaks a secret value. +describeEmbeddedPostgres("secretService dangling user_secret_ref resolution", () => { + let stopDb: (() => Promise) | null = null; + let db!: ReturnType; + const previousKeyFile = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; + const secretsTmpDir = path.join(os.tmpdir(), `paperclip-dangling-secret-${randomUUID()}`); + + beforeAll(async () => { + mkdirSync(secretsTmpDir, { recursive: true }); + process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = path.join(secretsTmpDir, "master.key"); + const started = await startEmbeddedPostgresTestDatabase("dangling-user-secret"); + stopDb = started.cleanup; + db = createDb(started.connectionString); + }); + + afterEach(async () => { + await db.delete(activityLog); + await db.delete(secretAccessEvents); + await db.delete(userSecretDeclarations); + await db.delete(companySecretBindings); + await db.delete(companySecretVersions); + await db.delete(companySecrets); + await db.delete(userSecretDefinitions); + await db.delete(companyMemberships); + await db.delete(companies); + }); + + afterAll(async () => { + if (stopDb) await stopDb(); + if (previousKeyFile === undefined) { + delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; + } else { + process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE = previousKeyFile; + } + rmSync(secretsTmpDir, { recursive: true, force: true }); + }); + + async function seedCompany(name = "Acme") { + const companyId = randomUUID(); + await db.insert(companies).values({ + id: companyId, + name, + issuePrefix: `T${companyId.slice(0, 7)}`.toUpperCase(), + status: "active", + createdAt: new Date(), + updatedAt: new Date(), + }); + return companyId; + } + + async function seedCompanyMember(companyId: string, userId: string) { + await db.insert(companyMemberships).values({ + companyId, + principalType: "user", + principalId: userId, + status: "active", + membershipRole: "owner", + createdAt: new Date(), + updatedAt: new Date(), + }); + } + + it("save path (syncEnvBindingsForTarget) names the env variable, the agent id, and the deleted definition key", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + const definition = await svc.createUserSecretDefinition(companyId, { + key: "github_token", + name: "GitHub token", + provider: "local_encrypted", + }); + + // Delete the definition, then keep an agent config that still references it. + await svc.removeUserSecretDefinition(companyId, definition.id); + + const agentId = randomUUID(); + const envValue = { + GH_TOKEN: { + type: "user_secret_ref" as const, + key: "github_token", + version: "latest" as const, + required: true, + }, + }; + + await expect( + svc.syncEnvBindingsForTarget(companyId, { targetType: "agent", targetId: agentId }, envValue), + ).rejects.toMatchObject({ + status: 404, + message: expect.stringContaining("User secret definition not found"), + }); + + // The message must name the env variable, the agent id, and the definition key. + const error = await svc + .syncEnvBindingsForTarget(companyId, { targetType: "agent", targetId: agentId }, envValue) + .catch((caught: unknown) => caught as Error); + expect(error).toBeInstanceOf(Error); + expect(error.message).toContain('environment variable "GH_TOKEN"'); + expect(error.message).toContain(`agent ${agentId}`); + expect(error.message).toContain('definition key "github_token"'); + }); + + it("Test path (resolveUserSecretValue) names the env variable, the agent id, and the deleted definition key without leaking the value", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + const definition = await svc.createUserSecretDefinition(companyId, { + key: "github_token", + name: "GitHub token", + provider: "local_encrypted", + }); + const secretValue = "ghp_super_secret_value"; + await svc.createCurrentUserSecretValue(companyId, "user-1", { + definitionId: definition.id, + value: secretValue, + }); + + // Delete the definition after the value exists. The binding now dangles. + await svc.removeUserSecretDefinition(companyId, definition.id); + + const agentId = randomUUID(); + const error = await svc + .resolveUserSecretValue( + companyId, + { definitionKey: "github_token", responsibleUserId: "user-1", required: true }, + { + consumerType: "agent", + consumerId: agentId, + configPath: "env.GH_TOKEN", + responsibleUserId: "user-1", + }, + ) + .catch((caught: unknown) => caught as Error); + + expect(error).toBeInstanceOf(Error); + expect((error as { status?: number }).status).toBe(404); + expect(error.message).toContain("User secret definition not found"); + expect(error.message).toContain('environment variable "GH_TOKEN"'); + expect(error.message).toContain(`agent ${agentId}`); + expect(error.message).toContain('definition key "github_token"'); + // The enriched message must never include the secret value. + expect(error.message).not.toContain(secretValue); + }); + + it("optional binding still resolves to null when the definition is missing (enrichment keeps the 404 status)", async () => { + const companyId = await seedCompany(); + await seedCompanyMember(companyId, "user-1"); + const svc = secretService(db); + + const resolved = await svc.resolveUserSecretValue( + companyId, + { definitionKey: "never_created", responsibleUserId: "user-1", required: false }, + { consumerType: "agent", consumerId: randomUUID(), configPath: "env.GH_TOKEN" }, + ); + expect(resolved).toBeNull(); + }); +}); diff --git a/server/src/__tests__/server-startup-feedback-export.test.ts b/server/src/__tests__/server-startup-feedback-export.test.ts index b01ad17d02..c4242eb552 100644 --- a/server/src/__tests__/server-startup-feedback-export.test.ts +++ b/server/src/__tests__/server-startup-feedback-export.test.ts @@ -68,6 +68,7 @@ const { reconcileTaskWatchdogs: vi.fn(async () => ({ triggered: 0 })), scanSilentActiveRuns: vi.fn(async () => ({ created: 0, escalated: 0 })), sweepStaleIssueLocks: vi.fn(async () => ({ cleared: 0 })), + sweepPendingCleanupLeases: vi.fn(async () => ({ swept: 0, destroyed: 0, capped: 0 })), reconcileProductivityReviews: vi.fn(async () => ({ created: 0, updated: 0, failed: 0 })), sweepExpiredRuntimeStatuses: vi.fn(() => 0), tickTimers: vi.fn(async () => ({ checked: 0, enqueued: 0, skipped: 0 })), @@ -522,7 +523,11 @@ describe("startServer feedback export wiring", () => { try { await startServer(); - expect(heartbeatServiceFactoryMock).not.toHaveBeenCalled(); + // The disabled path still creates one heartbeat runtime. This runtime owns + // the orphan-sandbox cleanup sweep, so a leaked provider sandbox is still + // reaped at startup and on the interval. + expect(heartbeatServiceFactoryMock).toHaveBeenCalledTimes(1); + expect(heartbeatServiceMock.sweepPendingCleanupLeases).toHaveBeenCalled(); expect(intervalCallback).not.toBeNull(); intervalCallback?.(); await Promise.resolve(); diff --git a/server/src/app.ts b/server/src/app.ts index 12efeea694..d9f2e6d833 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -31,6 +31,15 @@ import { statusCardRoutes } from "./routes/status-cards.js"; import { teamsCatalogRoutes } from "./routes/teams-catalog.js"; import { agentRoutes } from "./routes/agents.js"; import type { SetupTokenSessionService } from "./services/setup-token-session.js"; +import { + buildSetupTokenLoginTransport, + createProductionSetupTokenSandboxProvider, + createProductionSetupTokenCleanupStore, + createSetupTokenSecretWriter, + createWorkerBoundSetupTokenPtyOpener, +} from "./services/setup-token-transport-binding.js"; +import { environmentService } from "./services/environments.js"; +import { environmentRuntimeService } from "./services/environment-runtime.js"; import { projectRoutes } from "./routes/projects.js"; import { issueRoutes } from "./routes/issues.js"; import { issueTreeControlRoutes } from "./routes/issue-tree-control.js"; @@ -426,18 +435,52 @@ export async function createApp( .split(",") .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); - // The production server does not bind `setupTokenLogin` yet, so the start route - // fails closed with a fixed no-secret 503 and the login never spawns a process - // or holds a lease. This is a deliberate staged rollout: the live transport - // needs a real sandbox-lease manager, a live pseudo-terminal factory over the - // sandbox provider, and a durable cleanup store (the in-router default store is - // in-memory only). Each of those is a separate follow-up that goes through its - // own security review before the production server binds `setupTokenLogin`. + // Bind the production setup-token login transport. It carries the live lease + // manager, the login-process factory over the sandbox pseudo-terminal, and the + // durable cleanup store. The factory passes only the fixed command + // `CLAUDE_SETUP_TOKEN_COMMAND`; it never reads a command from a + // route, a request body, or an adapter configuration. The durable store and the + // startup reaper are live now, so a restart reaps a leftover lease. + // + // The live sandbox pseudo-terminal opener binds inside the sandbox provider + // worker, so the server process does not hold the raw sandbox process. The + // opener drives the worker through the plugin worker manager route gate. + // The manager mints a host-owned route identifier, permits one + // active credential pseudo-terminal per worker, binds the worker session + // identifier one time for output only, and terminalizes the route on every open + // failure path. With the opener supplied, the provider acquires a lease and the + // start route drives a live login instead of the fixed 503. + const setupTokenLoginTransport = buildSetupTokenLoginTransport({ + sandbox: createProductionSetupTokenSandboxProvider({ + environments: environmentService(db), + environmentRuntime: environmentRuntimeService(db, { pluginWorkerManager: workerManager }), + openLivePtySession: createWorkerBoundSetupTokenPtyOpener({ + workerManager, + environments: environmentService(db), + log: (line) => logger.info(line), + }), + log: (line) => logger.info(line), + }), + store: createProductionSetupTokenCleanupStore(db), + // Bind the atomic credential-claim writer, so a completed login transitions + // the durable row to `stored` and stores the minted token in one control-plane + // transaction. The writer reads the company and the owner only from the + // immutable session scope. The confirm-replacement flow owns rotation. Without + // this writer the router falls back to the deferred, fail-closed 503 and never + // stores the token. + completeCredential: createSetupTokenSecretWriter({ db }), + // Forward the login runner diagnostic lines to the server logger. The + // runner is the sole producer, and every line is a fixed, non-secret + // literal. Without this sink the diagnostics fall back to a no-op in + // production, so a failed login leaves no log trail. + log: (line) => logger.info(line), + }); api.use( agentRoutes(db, { pluginWorkerManager: workerManager, deploymentMode: opts.deploymentMode, confidentialProxyAllowlist: setupTokenLoginProxyAllowlist, + setupTokenLogin: setupTokenLoginTransport, onSetupTokenLoginService: (service) => { setupTokenLoginService = service; // Startup reaper (SR-4): release any lease whose login session is @@ -865,28 +908,40 @@ export async function createApp( logger.error({ err }, "Failed to load ready plugins on startup"); }); app.locals.bundledPluginsStartup = bundledPluginsStartup; - let appServicesShutdown = false; - const shutdownAppServices = () => { - if (appServicesShutdown) return; - appServicesShutdown = true; - disableFeedbackExportFlushes(); - if (importTransferSweepTimer) { - clearInterval(importTransferSweepTimer); - importTransferSweepTimer = null; - } - devWatcher?.close(); - viteHtmlRenderer?.dispose(); - void viteDevServer?.close().catch(() => undefined); - viteHmrServer?.close(); - hostServiceCleanup.disposeAll(); - hostServiceCleanup.teardown(); - // Cancel every live setup-token login session, so each direct child stops - // before the server releases each lease (SR-4). - void setupTokenLoginService?.shutdown(); + // The shutdown hook runs at most once. It caches the in-flight promise, so a + // second caller (for example the `exit` handler) awaits the same completion + // instead of starting a second teardown. + let appServicesShutdown: Promise | null = null; + const shutdownAppServices = (): Promise => { + if (appServicesShutdown) return appServicesShutdown; + appServicesShutdown = (async () => { + disableFeedbackExportFlushes(); + if (importTransferSweepTimer) { + clearInterval(importTransferSweepTimer); + importTransferSweepTimer = null; + } + devWatcher?.close(); + viteHtmlRenderer?.dispose(); + void viteDevServer?.close().catch(() => undefined); + viteHmrServer?.close(); + hostServiceCleanup.disposeAll(); + hostServiceCleanup.teardown(); + // Cancel every live setup-token login session and AWAIT the cancellation, + // so each direct child stops and the server releases each lease before the + // caller stops the database and the provider. A lease release that + // fails stays a durable record for the startup reaper. + await setupTokenLoginService?.shutdown(); + })(); + return appServicesShutdown; }; app.locals.paperclipShutdown = shutdownAppServices; - process.once("exit", shutdownAppServices); + // The `exit` event is synchronous. It cannot await the teardown, so it runs + // the best-effort cleanup and drops the returned promise. The orderly signal + // path awaits `shutdownAppServices` in full before the process exits. + process.once("exit", () => { + void shutdownAppServices(); + }); process.once("beforeExit", () => { void flushPluginLogBuffer(); }); diff --git a/server/src/index.ts b/server/src/index.ts index e5e5410a62..c399f1b8b8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -91,6 +91,7 @@ import { ensureDecisionSigningSecret } from "./services/decision-signing.js"; import { createDecisionRetentionNotifyOriginAgent, createDecisionWakeOriginAgent } from "./services/decision-wakeup.js"; import { coordinateHeartbeatSchedulerShutdown, + finalizeServerShutdown, loadWithoutCoordinatedShutdownSignalHooks, } from "./shutdown.js"; import { systemdNotify } from "./services/systemd-notify.js"; @@ -991,6 +992,46 @@ export async function startServer(): Promise { })); }; + // The retry backstop for orphan sandboxes. An acquire that rejects a + // foreign-company insert tears the provisioned sandbox down. If that teardown + // also fails, the acquire records a lease-less `pending_cleanup` lease row. No + // other path releases that sandbox, so the master pending-cleanup sweep retries + // the provider teardown and releases the orphan. The sweep runs on startup and + // on the scheduler interval. + // + // This backstop is independent of the heartbeat scheduler toggle. A leaked + // provider sandbox costs money whether or not the instance schedules + // heartbeats, so both the enabled and the disabled path run the sweep. A + // disabled heartbeat scheduler must not strand a paid sandbox forever. + // + // The master pending-cleanup sweep is the single owner of these rows. Its + // atomic per-attempt claim makes two overlapping sweeps safe, so the enabled + // path can also run the sweep from the orphaned-run reaper without a second + // teardown. The heartbeat scheduler owns the sweep when it is enabled; the + // disabled path creates its own runtime to own the same sweep. + // The interval sweep waits this long after a lease's last write before it + // retries the teardown. The window matches the orphaned-run reaper staleness, + // so a just-failed lease does not draw a retry on every tick. The startup + // sweep passes zero, so a restart retries a stranded orphan at once. + const ENVIRONMENT_LEASE_CLEANUP_SWEEP_BACKOFF_MS = 5 * 60 * 1000; + const environmentLeaseCleanupHeartbeat = + heartbeat ?? heartbeatService(db as any, { pluginWorkerManager }); + const runEnvironmentLeaseCleanupSweep = (backoffMs: number) => + environmentLeaseCleanupHeartbeat + .sweepPendingCleanupLeases({ backoffMs }) + .then((result) => { + if (result.destroyed > 0 || result.capped > 0) { + logger.info(result, "environment lease cleanup sweep retried orphan sandbox teardowns"); + } + }) + .catch((err) => { + logger.error({ err }, "environment lease cleanup sweep failed"); + }); + const scheduleEnvironmentLeaseCleanupSweep = () => { + if (heartbeatSchedulerStopped) return; + trackHeartbeatSchedulerWork(runEnvironmentLeaseCleanupSweep(ENVIRONMENT_LEASE_CLEANUP_SWEEP_BACKOFF_MS)); + }; + if (heartbeat) { const secretProposals = createSecretProposalsService(db as any); const decisionExecutor = decisionService(db as any, decisionServiceOptions); @@ -1086,6 +1127,7 @@ export async function startServer(): Promise { logger.error({ err }, "adapter login reaper sweep failed"); })); }; + const tools = toolAccessService(db as any, { deploymentMode: config.deploymentMode, deploymentExposure: config.deploymentExposure, @@ -1223,6 +1265,10 @@ export async function startServer(): Promise { logger.error({ err }, "startup adapter login reaper sweep failed"); }); + // Retry any orphan sandbox teardown left by a failed acquire before a server + // restart, so a leaked sandbox does not stay allocated across the restart. + await runEnvironmentLeaseCleanupSweep(0); + const runRetentionSweep = async () => { const activeCompanies = await db.select({ id: companies.id }).from(companies).where(eq(companies.status, "active")); let archived = 0; @@ -1282,6 +1328,7 @@ export async function startServer(): Promise { scheduleMergedPullRequestConfirmationSweep(); scheduleTerminalWorkspaceSweep(); scheduleAdapterLoginReaperSweep(); + scheduleEnvironmentLeaseCleanupSweep(); if (heartbeatSchedulerStopped) return; trackHeartbeatSchedulerWork(routines @@ -1419,8 +1466,14 @@ export async function startServer(): Promise { })); }); } else { + // The heartbeat scheduler is disabled, but the orphan-sandbox cleanup sweep + // is still required. A failed acquire can leak a paid provider sandbox, so + // this path retries the teardown at startup and on the interval, exactly as + // the enabled path does. + await runEnvironmentLeaseCleanupSweep(0); startHeartbeatSchedulerInterval(() => { scheduleExternalObjectRefreshSweep(new Date()); + scheduleEnvironmentLeaseCleanupSweep(); }); } @@ -1577,21 +1630,22 @@ export async function startServer(): Promise { logger.error({ err, signal }, "run-log in-flight mirror flush failed"); } - const appShutdown = (app as { locals?: { paperclipShutdown?: () => void } }).locals?.paperclipShutdown; - appShutdown?.(); + const appShutdown = (app as { locals?: { paperclipShutdown?: () => Promise } }).locals + ?.paperclipShutdown; + const embeddedPostgresToStop = + embeddedPostgres && embeddedPostgresStartedByThisProcess ? embeddedPostgres : null; - if (embeddedPostgres && embeddedPostgresStartedByThisProcess) { - logger.info({ signal }, "Stopping embedded PostgreSQL"); - try { - await embeddedPostgres?.stop(); - } catch (err) { - logger.error({ err }, "Failed to stop embedded PostgreSQL cleanly"); - } - } - - // Flush buffered OTel spans before the process goes away; without this - // await the exporter's final batch is dropped on exit. - await shutdownInstrumentation(); + // Await the ordered application teardown before the process exits. A live + // setup-token login session must stop and release its sandbox lease before + // the database and the provider stop, so an orderly shutdown never leaves a + // sandbox lease or confidential login state alive past the process exit. + await finalizeServerShutdown({ + signal, + shutdownAppServices: appShutdown, + stopEmbeddedPostgres: embeddedPostgresToStop ? () => embeddedPostgresToStop.stop() : null, + shutdownInstrumentation, + log: logger, + }); process.exit(0); }; diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 7ff546cb9f..25d2e16071 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -29,6 +29,8 @@ import { updateAgentSchema, supportedEnvironmentDriversForAdapter, LOW_TRUST_REVIEW_PRESET, + startClaudeSetupTokenSessionRequestSchema, + submitBrowserCodeRequestSchema, } from "@paperclipai/shared"; import { isForbiddenConfigEnvKey, @@ -58,7 +60,7 @@ import { } from "../services/index.js"; import { badRequest, conflict, forbidden, HttpError, notFound, unprocessable } from "../errors.js"; import { createRunSecretRedactionRegistry } from "../services/run-secret-redaction.js"; -import { assertBoard, assertCompanyAccess, assertInstanceAdmin, buildActorSecretContext, getAccessibleResource, getActorInfo, hasCompanyAccess } from "./authz.js"; +import { assertAuthenticated, assertBoard, assertCompanyAccess, assertInstanceAdmin, buildActorSecretContext, getAccessibleResource, getActorInfo, hasCompanyAccess } from "./authz.js"; import { assertNoAgentHostWorkspaceCommandMutation, collectAgentAdapterWorkspaceCommandPaths, @@ -67,6 +69,7 @@ import type { PluginWorkerManager } from "../services/plugin-worker-manager.js"; import { environmentService } from "../services/environments.js"; import { resolveEnvironmentExecutionTarget } from "../services/environment-execution-target.js"; import { environmentRuntimeService } from "../services/environment-runtime.js"; +import { resolvePluginSandboxProviderDriverByKey } from "../services/plugin-environment-driver.js"; import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target"; import type { AdapterEnvironmentCheck, @@ -103,16 +106,33 @@ import { assessConfidentialStartup, evaluateConfidentialTransport, SETUP_TOKEN_START_FAILED, - SETUP_TOKEN_TRANSPORT_INSECURE, + SETUP_TOKEN_SESSION_NOT_FOUND, + SETUP_TOKEN_PROVIDER_UNSUPPORTED, + SETUP_TOKEN_PROVIDER_UNSUPPORTED_CODE, type ConfidentialTransportConfig, type SetupTokenCleanupRecord, type SetupTokenCleanupStore, type SetupTokenLease, type SetupTokenLeaseManager, type SetupTokenLoginProcessFactory, + type SetupTokenSecretWriter, type SetupTokenSessionScope, + type SetupTokenSessionState, + type SetupTokenSessionDescriptor, } from "../services/setup-token-session.js"; -import type { DeploymentMode } from "@paperclipai/shared"; +import type { + DeploymentMode, + AdapterAuthSessionStatus, + AdapterAuthSessionFailure, + ClaudeSetupTokenSessionResponse, + ClaudeSetupTokenSessionOwnerResponse, + ClaudeSetupTokenSessionPrompt, + ClaudeSetupTokenCompletionResponse, + ClaudeOAuthTokenStatusResponse, + ClaudeSetupTokenOverwrite, + SetupTokenTransportAdvisory, +} from "@paperclipai/shared"; +import { SETUP_TOKEN_TRANSPORT_ADVISORY_CODE } from "@paperclipai/shared"; import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "@paperclipai/adapter-codex-local"; import { checkStagedCredentialReadiness, @@ -233,6 +253,11 @@ export function agentRoutes( leases: SetupTokenLeaseManager; /** The durable cleanup store. Defaults to the in-memory record store. */ store?: SetupTokenCleanupStore; + /** + * The owner-bound secret writer. When the caller omits it, the completion + * fails closed, because the secret sink is not bound yet. + */ + completeCredential?: SetupTokenSecretWriter; }; } = {}, ) { @@ -325,20 +350,47 @@ export function agentRoutes( // The in-memory non-secret cleanup record store. It is the default store when a // caller does not inject a durable database-backed store. const setupTokenCleanupRows = new Map(); + const scopeMatchesRow = (row: SetupTokenCleanupRecord, identity: { + companyId: string; + ownerUserId: string; + adapterType: string; + environmentId: string; + }): boolean => + row.companyId === identity.companyId && + row.ownerUserId === identity.ownerUserId && + row.adapterType === identity.adapterType && + row.environmentId === identity.environmentId; const inMemorySetupTokenCleanupStore: SetupTokenCleanupStore = { async record(record): Promise { setupTokenCleanupRows.set(record.sessionId, { ...record }); }, - async markState(sessionId, state): Promise { - const row = setupTokenCleanupRows.get(sessionId); - if (row) row.state = state; + async markState(identity, state): Promise { + const row = setupTokenCleanupRows.get(identity.sessionId); + if (row && scopeMatchesRow(row, identity)) row.state = state; }, - async remove(sessionId): Promise { - setupTokenCleanupRows.delete(sessionId); + async remove(identity): Promise { + // The delete matches the full owner scope, so it never removes a row by the + // session id alone. + const row = setupTokenCleanupRows.get(identity.sessionId); + if (row && scopeMatchesRow(row, identity)) setupTokenCleanupRows.delete(identity.sessionId); }, async listReapable(): Promise { return []; }, + async consumeStoredClaim(identity): Promise { + const row = setupTokenCleanupRows.get(identity.sessionId); + if ( + !row || + !scopeMatchesRow(row, identity) || + row.state !== "stored" || + row.boundAt !== null || + row.deadline <= Date.now() + ) { + return null; + } + row.boundAt = Date.now(); + return { ...row }; + }, }; const deferredSetupTokenLoginFactory: SetupTokenLoginProcessFactory = () => { @@ -347,19 +399,49 @@ export function agentRoutes( throw new SetupTokenSessionError(503, SETUP_TOKEN_START_FAILED); }; - // Resolve the transport: use the injected factory, lease manager, and store - // when a caller binds them; otherwise use the deferred, fail-closed defaults. + const deferredSetupTokenSecretWriter: SetupTokenSecretWriter = async () => { + // The owner-bound secret writer arrives through `options.setupTokenLogin`. + // Until then the completion fails closed, so the session never reports a + // stored credential without a real secret write. + throw new SetupTokenSessionError(503, SETUP_TOKEN_START_FAILED); + }; + + // Resolve the transport: use the injected factory, lease manager, store, and + // secret writer when a caller binds them; otherwise use the deferred, + // fail-closed defaults. const setupTokenLoginFactory = options.setupTokenLogin?.factory ?? deferredSetupTokenLoginFactory; const setupTokenLeaseManager = options.setupTokenLogin?.leases ?? deferredSetupTokenLeaseManager; const setupTokenCleanupStore = options.setupTokenLogin?.store ?? inMemorySetupTokenCleanupStore; + const setupTokenSecretWriter = + options.setupTokenLogin?.completeCredential ?? deferredSetupTokenSecretWriter; + + // Re-check the environment company binding at lease acquisition. The start + // route runs `assertSandboxLoginEnvironment` before the session begins, but + // managed-environment reconciliation can bind the sandbox to another company + // between that guard and the lease acquire. This wrapper re-runs the same + // guard at acquire time and fails closed with the 403 + // `environment_company_mismatch` before the transport provisions a sandbox. + // The lease insert transaction re-checks the binding once more inside the + // insert, so a bind that lands during the provider call still holds no lease. + const guardedSetupTokenLeaseManager: SetupTokenLeaseManager = { + async acquire(input): Promise { + await assertSandboxLoginEnvironment(input.scope.companyId, input.scope.environmentId, { + requireSetupTokenLoginProvider: true, + }); + return setupTokenLeaseManager.acquire(input); + }, + release: (lease) => setupTokenLeaseManager.release(lease), + releaseById: (leaseId) => setupTokenLeaseManager.releaseById(leaseId), + }; const setupTokenLoginService = new SetupTokenSessionService({ factory: setupTokenLoginFactory, - leases: setupTokenLeaseManager, + leases: guardedSetupTokenLeaseManager, store: setupTokenCleanupStore, + completeCredential: setupTokenSecretWriter, rateLimiter: setupTokenRateLimiter, }); @@ -1088,13 +1170,69 @@ export function agentRoutes( // only in an active sandbox environment. This reuses the shared environment // selection guard, so it rejects a missing, archived (inactive), local, SSH, or // plugin environment the same way the agent configuration routes do. + // + // The execution environment catalog is instance-scoped, not company-owned. PR + // #8375 moved the catalog to one shared instance catalog, so an environment row + // carries no single owning company. The shared selection guard therefore checks + // only the driver and the status. The company-binding check below then rejects + // an environment that binds to other companies. The route caller is already + // bound to the path company by `assertCompanyAccess`, and the acquired lease + // records that same company, so the login stays attributed to the caller. async function assertSandboxLoginEnvironment( companyId: string, environmentId: string, + options?: { requireSetupTokenLoginProvider?: boolean }, ): Promise { await assertEnvironmentSelectionForCompany(environmentsSvc, companyId, environmentId, { allowedDrivers: ["sandbox"], }); + // Reject an environment that another company owns. A managed sandbox + // environment binds to the companies that the instance provisions it for. + // When the environment binds to companies but not the request company, the + // environment belongs to another company. A login there runs the process in + // a foreign company sandbox, so the guard fails closed. An environment with + // no company binding is instance-global and stays open to every member. + const boundCompanyIds = await environmentsSvc.listBoundCompanyIds(environmentId); + if (boundCompanyIds.length > 0 && !boundCompanyIds.includes(companyId)) { + throw forbidden("The selected environment belongs to another company.", { + code: "environment_company_mismatch", + }); + } + // Gate the Claude setup-token login on the provider capability. Only a + // sandbox provider that advertises the setup-token login capability + // implements the setup-token pseudo-terminal methods. The setup-token start + // routes pass this option, so an unsupported provider fails closed here + // before the session starts. The lease guard passes it too, so a + // reconciliation that rebinds the environment to an unsupported provider + // still fails closed before the lease and the pseudo-terminal. + if (options?.requireSetupTokenLoginProvider) { + await assertSetupTokenLoginProviderCapability(environmentId); + } + } + + /** + * Fails closed when the environment provider does not advertise the Claude + * setup-token login capability. It resolves the effective provider from the + * environment config, then reads the static capability from the provider + * plugin manifest. It never checks the provider by name. A missing plugin, a + * non-plugin provider, and a provider without the flag all fail closed with + * the fixed, typed error, so no session row, lease, or pseudo-terminal starts. + */ + async function assertSetupTokenLoginProviderCapability(environmentId: string): Promise { + const environment = await environmentsSvc.getById(environmentId); + const config = + environment?.config && typeof environment.config === "object" + ? (environment.config as Record) + : {}; + const provider = typeof config.provider === "string" ? config.provider : ""; + const resolved = provider + ? await resolvePluginSandboxProviderDriverByKey({ db, driverKey: provider }) + : null; + if (!resolved?.driver.supportsSetupTokenLogin) { + throw unprocessable(SETUP_TOKEN_PROVIDER_UNSUPPORTED, { + code: SETUP_TOKEN_PROVIDER_UNSUPPORTED_CODE, + }); + } } // Read a login session for its owner. The durable row is the authority for the @@ -2976,6 +3114,13 @@ export function agentRoutes( instructionsBundle, sourceIssueId: _sourceIssueId, sourceIssueIds: _sourceIssueIds, + // The stored-session claim is not an agent column. The server derives the + // owner from the authenticated actor and consumes the claim in the create + // transaction, so it never reaches the insert values. + storedSessionId: hireStoredSessionId, + // The apply-existing flag is not an agent column. The server binds the + // fixed reference to the owner stored value with no login round trip. + applyStoredClaudeLogin: hireApplyStoredClaudeLogin, ...hireInput } = req.body; hireInput.adapterType = assertSelectableAdapterType(hireInput.adapterType); @@ -3032,13 +3177,26 @@ export function agentRoutes( const requiresApproval = company.requireBoardApprovalForNewAgents; const status = requiresApproval ? "pending_approval" : "idle"; - const createdAgent = await svc.create(companyId, { - id: hiredAgentId, - ...normalizedHireInput, - status, - spentMonthlyCents: 0, - lastHeartbeatAt: null, - }); + const createdAgent = await svc.create( + companyId, + { + id: hiredAgentId, + ...normalizedHireInput, + status, + spentMonthlyCents: 0, + lastHeartbeatAt: null, + }, + { + claudeLogin: { + storedSessionId: hireStoredSessionId ?? null, + ownerUserId: req.actor.type === "agent" ? null : (req.actor.userId ?? null), + // The apply-existing path runs only for a user actor. The owner comes + // from the actor, so an agent actor never reaches the no-claim bind. + applyExistingWithoutClaim: + req.actor.type !== "agent" && hireApplyStoredClaudeLogin === true, + }, + }, + ); const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent, instructionsBundle); let approval: Awaited> | null = null; @@ -3172,6 +3330,13 @@ export function agentRoutes( const { desiredSkills: requestedDesiredSkills, instructionsBundle, + // The stored-session claim is not an agent column. The server derives the + // owner from the authenticated actor and consumes the claim in the create + // transaction, so it never reaches the insert values. + storedSessionId: createStoredSessionId, + // The apply-existing flag is not an agent column. The server binds the + // fixed reference to the owner stored value with no login round trip. + applyStoredClaudeLogin: createApplyStoredClaudeLogin, ...createInput } = req.body; createInput.adapterType = assertSelectableAdapterType(createInput.adapterType); @@ -3216,15 +3381,28 @@ export function agentRoutes( allowedSandboxProviders: allowedSandboxProvidersForAgent(createInput.adapterType), }); - const createdAgent = await svc.create(companyId, { - id: agentId, - ...createInput, - adapterConfig: normalizedAdapterConfig, - runtimeConfig: normalizedRuntimeConfig, - status: "idle", - spentMonthlyCents: 0, - lastHeartbeatAt: null, - }); + const createdAgent = await svc.create( + companyId, + { + id: agentId, + ...createInput, + adapterConfig: normalizedAdapterConfig, + runtimeConfig: normalizedRuntimeConfig, + status: "idle", + spentMonthlyCents: 0, + lastHeartbeatAt: null, + }, + { + claudeLogin: { + storedSessionId: createStoredSessionId ?? null, + ownerUserId: req.actor.type === "agent" ? null : (req.actor.userId ?? null), + // The apply-existing path runs only for a user actor. The owner comes + // from the actor, so an agent actor never reaches the no-claim bind. + applyExistingWithoutClaim: + req.actor.type !== "agent" && createApplyStoredClaudeLogin === true, + }, + }, + ); const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent, instructionsBundle); const actor = getActorInfo(req); @@ -3572,6 +3750,11 @@ export function agentRoutes( const patchData = { ...(req.body as Record) }; const replaceAdapterConfig = patchData.replaceAdapterConfig === true; delete patchData.replaceAdapterConfig; + // The apply-existing flag is not an agent column. The server binds the fixed + // reference to the owner stored value with no login round trip. Remove it + // from the patch so it never reaches the update values. + const applyStoredClaudeLogin = patchData.applyStoredClaudeLogin === true; + delete patchData.applyStoredClaudeLogin; if (hasOwn(patchData, "adapterConfig")) { const adapterConfig = asRecord(patchData.adapterConfig); if (!adapterConfig) { @@ -3698,6 +3881,13 @@ export function agentRoutes( createdByUserId: actor.actorType === "user" ? actor.actorId : null, source: "patch", }, + claudeLogin: { + ownerUserId: req.actor.type === "agent" ? null : (req.actor.userId ?? null), + // The apply-existing path runs only for a user actor. The owner comes + // from the actor, so an agent actor never reaches the no-claim bind. + applyExistingWithoutClaim: + req.actor.type !== "agent" && applyStoredClaudeLogin, + }, }); if (!agent) { res.status(404).json({ error: "Agent not found" }); @@ -4229,11 +4419,10 @@ export function agentRoutes( // --- Setup-token login session routes -------------------------------------- // - // The routes give the harness the operations against one live login session. - // Every operation verifies the company, the owner user, and the target agent - // through the session scope. A missing session and a cross-scope session both - // return the same 404. The confidential responses (read-prompt and - // receive-token) pass through the fail-closed transport guard and set + // The routes give the UI operations against one live login session. Every + // operation verifies the company and owner user through the session scope. A + // missing session and a cross-scope session both return the same 404. The + // confidential responses pass through the transport assessment and set // `Cache-Control: no-store`. The routes write no prompt, code, token, or raw // process chunk to a log or an activity detail, and they return fixed error // text only. @@ -4251,28 +4440,63 @@ export function agentRoutes( // test with an unresolved path, so the routes repeat the base path instead. /** - * Builds the immutable session scope from the authenticated owner user and the - * target agent (SR-3, FU-1). Only a user actor owns a login session; the route - * uses this dedicated login scope, not the broad agent-manage permission. + * Derives the immutable owner of a setup-token login session from the actor. + * Only a board user owns a login session. It returns the owner id, or it + * throws a forbidden error. The owner is never a client field; it comes only + * from the authenticated actor. */ - const buildSetupTokenScope = ( - req: Request, - agent: { id: string; companyId: string }, - ): SetupTokenSessionScope => { + const deriveSetupTokenOwnerUserId = (req: Request): string => { const actor = getActorInfo(req); if (actor.actorType !== "user") { throw forbidden("A user must own a setup-token login session."); } - return { companyId: agent.companyId, ownerUserId: actor.actorId, targetAgentId: agent.id }; + return actor.actorId; }; /** - * Applies the fail-closed confidential transport guard (SR-6, SR-7). It reads - * the raw socket TLS bit and the immediate peer address, so the global - * `trust proxy` setting cannot influence the decision. It returns false and - * sends the fixed no-secret error when the transport is not confidential. + * Read-access gate for the company-scoped setup-token session routes. It runs + * before a route resolves a session. The session id is an opaque secret-bearing + * reference, so a cross-company reference must fail closed like a missing + * session. This gate returns the same fixed not-found error for a cross-company + * reference by an authenticated non-member as for a missing session, so the + * route is not a company-membership oracle. It keeps the not-found equivalence + * the session lookups use. + * + * The gate keeps the actor rules unchanged. It throws 401 for an unauthenticated + * caller and 403 for a non-user actor through the owner derivation. For an + * authorized member it runs the full `assertCompanyAccess` write-path checks and + * returns the owner user id. For a non-member it sends the fixed 404 and returns + * null; the route must stop. */ - const enforceSetupTokenTransport = (req: Request, res: Response): boolean => { + const resolveCompanySessionOwner = ( + req: Request, + companyId: string, + res: Response, + ): string | null => { + assertAuthenticated(req); + const ownerUserId = deriveSetupTokenOwnerUserId(req); + if (!hasCompanyAccess(req, companyId)) { + res.setHeader("Cache-Control", "no-store"); + res.status(404).json({ error: SETUP_TOKEN_SESSION_NOT_FOUND }); + return null; + } + assertCompanyAccess(req, companyId); + return ownerUserId; + }; + + /** + * Assesses the setup-token confidential transport. The product + * owner set a non-negotiable requirement: do not force TLS. Many users run + * Paperclip over plain HTTP on a home server or a Tailscale tailnet. So the + * route does not block a non-confidential transport. It returns a non-blocking + * advisory instead, and the route attaches it to the confidential response. + * The client shows a visible disclaimer and lets the login proceed. The + * function reads the raw socket TLS bit and the immediate peer address, so the + * global `trust proxy` setting cannot change the result. It returns null when + * the transport is confidential (direct TLS, a local-trusted loopback, or an + * allowlisted TLS proxy), so a confidential response shows no disclaimer. + */ + const assessSetupTokenTransport = (req: Request): SetupTokenTransportAdvisory | null => { const socket = req.socket as { encrypted?: boolean; remoteAddress?: string }; const forwardedProto = req.headers["x-forwarded-proto"]; const decision = evaluateConfidentialTransport(setupTokenConfidentialConfig, { @@ -4280,12 +4504,7 @@ export function agentRoutes( remoteAddress: socket?.remoteAddress, forwardedProto: Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto, }); - if (!decision.allowed) { - res.setHeader("Cache-Control", "no-store"); - res.status(403).json({ error: SETUP_TOKEN_TRANSPORT_INSECURE }); - return false; - } - return true; + return decision.allowed ? null : { code: SETUP_TOKEN_TRANSPORT_ADVISORY_CODE }; }; const sendSetupTokenError = (res: Response, err: unknown): void => { @@ -4296,123 +4515,324 @@ export function agentRoutes( throw err; }; - /** - * Resolves the target agent for a login-session route and verifies it is a - * `claude_local` agent. Returns null and sends the response when the agent is - * missing, inaccessible, or the wrong adapter type. - */ - const resolveSetupTokenAgent = async (req: Request, res: Response) => { - const id = req.params.id as string; - const agent = await getAccessibleResource(req, res, svc.getById(id), "Agent not found"); - if (!agent) return null; - if (agent.adapterType !== "claude_local") { - res.status(400).json({ error: "Login is only supported for claude_local agents" }); - return null; + // --- Company-and-environment setup-token login routes ---------------------- + // + // These routes serve the agentless Claude login. The scope binds one login to + // one company, one owner user, one adapter, and one environment. The scope + // carries no agent id, so a hire flow starts one login before an agent exists. + // + // Object-level authorization: every action derives the owner from + // the authenticated actor, fixes the adapter to `claude_local`, and resolves + // the environment server-side. The lookup scopes by the immutable tuple + // company, owner, adapter, environment, and session. A foreign session returns + // the same not-found error as a missing session, so a caller cannot enumerate + // a session across a company, an owner, an adapter, or an environment. + // + // Each route writes its full path as a plain string literal, so the static + // OpenAPI coverage test can read the path from the source text. + + // The company-and-environment login serves only the Claude adapter. + const CLAUDE_SETUP_TOKEN_ADAPTER_TYPE = "claude_local"; + + // Maps the internal session state to the public login status. The public union + // carries no server-only state, so the route never returns the internal + // `submitting` or `stored` state to a client. + const toClaudeLoginStatus = (state: SetupTokenSessionState): AdapterAuthSessionStatus => { + switch (state) { + case "starting": + return "starting"; + case "awaiting_code": + case "submitting": + case "stored": + return "waiting_for_user"; + case "completed": + return "authenticated"; + case "failed": + return "failed"; + case "timed_out": + return "timed_out"; + case "cancelled": + return "cancelled"; } - return agent; }; - router.post("/agents/:id/setup-token-login-sessions", async (req, res) => { - const agent = await resolveSetupTokenAgent(req, res); - if (!agent) return; - const scope = buildSetupTokenScope(req, agent); + // Builds the fixed, non-secret failure for a terminal failure state. A live or + // a completed session has no failure. The failure carries a stable reason and + // no secret detail. + const toClaudeLoginFailure = (state: SetupTokenSessionState): AdapterAuthSessionFailure | null => { + switch (state) { + case "failed": + return { reason: "failed", message: null }; + case "timed_out": + return { reason: "timed_out", message: null }; + case "cancelled": + return { reason: "cancelled", message: null }; + default: + return null; + } + }; + + // The public login-session response. It carries no prompt and no secret. + const toClaudePublicResponse = ( + descriptor: SetupTokenSessionDescriptor, + ): ClaudeSetupTokenSessionResponse => ({ + sessionId: descriptor.sessionId, + environmentId: descriptor.environmentId, + status: toClaudeLoginStatus(descriptor.state), + expiresAt: new Date(descriptor.deadline).toISOString(), + failure: toClaudeLoginFailure(descriptor.state), + }); + + // The company-and-environment login key the non-start routes derive. The route + // path gives the company, the actor gives the owner, and the route fixes the + // adapter. The service matches this key and the agentless marker. + const companySetupTokenKey = (companyId: string, ownerUserId: string) => ({ + companyId, + ownerUserId, + adapterType: CLAUDE_SETUP_TOKEN_ADAPTER_TYPE, + }); + + // The stored Claude OAuth token status read. It returns + // only the secret id and the latest version of the owner value; it returns no + // token. The client reads the version, applies the stored token first, and + // captures the version for a later confirmed overwrite. The route derives the + // owner only from the authenticated actor and reads the fixed Claude + // definition; it accepts no owner, no definition, and no secret id as input. + // + // The route returns the same fixed 404 for a missing owner value as the + // company gate returns for a non-member, so it discloses no existence + // distinction across owners or companies. It sets `Cache-Control: no-store`, + // so no cache holds the metadata. + router.get("/companies/:companyId/claude-oauth-token-status", async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; res.setHeader("Cache-Control", "no-store"); + const status = await secretsSvc.readClaudeOAuthUserSecretStatus(companyId, ownerUserId); + if (!status) { + // A missing owner value returns the same fixed not-found as the non-member + // gate, so a member without a value and a non-member look the same. + res.status(404).json({ error: SETUP_TOKEN_SESSION_NOT_FOUND }); + return; + } + const body: ClaudeOAuthTokenStatusResponse = status; + res.json(body); + }); + + router.post("/companies/:companyId/setup-token-login-sessions", async (req, res) => { + const companyId = req.params.companyId as string; + assertCompanyAccess(req, companyId); + const ownerUserId = deriveSetupTokenOwnerUserId(req); + res.setHeader("Cache-Control", "no-store"); + + // Parse the request with the shared strict validator before any session, + // lease, or pseudo-terminal side effect. `.strict()` rejects an unknown field, + // including a legacy `ttlSeconds`, with a fixed 400. The route echoes no input. + const parsed = startClaudeSetupTokenSessionRequestSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: "The Claude login start request is invalid." }); + return; + } + const { environmentId, adapterType } = parsed.data; + const confirmedOverwrite: ClaudeSetupTokenOverwrite | null = parsed.data.overwrite ?? null; + + // The company-and-environment login serves only the Claude adapter. A + // different adapter type gets a fixed 400. + if (adapterType !== CLAUDE_SETUP_TOKEN_ADAPTER_TYPE) { + res.status(400).json({ error: "Only the claude_local adapter supports a setup-token login." }); + return; + } + if (!SETUP_TOKEN_LOGIN_TRANSPORT_READY) { // The live login transport binds at a later call site. Fail closed with the // fixed no-secret error until then. res.status(503).json({ error: SETUP_TOKEN_START_FAILED }); return; } + + // Resolve the environment server-side to a live instance sandbox. It fails + // closed on a missing, archived, non-sandbox, or fake-provider environment, so + // the route never persists an empty or invalid environment scope. It runs + // before the session starts, so no store holds a rejected environment. It + // also rejects an environment that another company owns (see + // `assertSandboxLoginEnvironment`), so a login never runs in a foreign + // company sandbox. It also fails closed when the provider does not advertise + // the setup-token login capability, so an unsupported provider never reaches + // a session row, a lease, or a pseudo-terminal. + await assertSandboxLoginEnvironment(companyId, environmentId, { + requireSetupTokenLoginProvider: true, + }); + + const scope: SetupTokenSessionScope = { + companyId, + ownerUserId, + targetAgentId: null, + adapterType, + environmentId, + confirmedOverwrite, + }; try { const started = await setupTokenLoginService.start(scope); - res.status(201).json({ sessionId: started.sessionId, state: started.state }); + const descriptor = setupTokenLoginService.describeOwned(started.sessionId, scope); + // The start response carries the panel mode, so the client renders the + // browser-code panel. The full login URL rides only through the + // guarded prompt read, not the start response, so the prompt is null here. + // The client reads the prompt route for the login URL. + const body: ClaudeSetupTokenSessionOwnerResponse = { + ...toClaudePublicResponse(descriptor), + panelMode: "submitted_browser_code", + prompt: null, + }; + res.status(201).json(body); } catch (err) { sendSetupTokenError(res, err); } }); - router.get("/agents/:id/setup-token-login-sessions/:sessionId/prompt", async (req, res) => { - const agent = await resolveSetupTokenAgent(req, res); - if (!agent) return; - const scope = buildSetupTokenScope(req, agent); + router.get("/companies/:companyId/setup-token-login-sessions/:sessionId", async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; res.setHeader("Cache-Control", "no-store"); - // SR-6 and SR-7: the full login URL is a confidential response. - if (!enforceSetupTokenTransport(req, res)) return; try { - const view = setupTokenLoginService.readPrompt(req.params.sessionId as string, scope); - // SR-5: the full login URL rides only in this authorized owner response. - res.json({ state: view.state, loginUrl: view.loginUrl }); + const sessionId = req.params.sessionId as string; + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + const descriptor = setupTokenLoginService.describeOwned(sessionId, scope); + // The status response is public. It carries no prompt and no secret. + res.json(toClaudePublicResponse(descriptor)); } catch (err) { sendSetupTokenError(res, err); } }); - router.post("/agents/:id/setup-token-login-sessions/:sessionId/code", async (req, res) => { - const agent = await resolveSetupTokenAgent(req, res); - if (!agent) return; - const scope = buildSetupTokenScope(req, agent); - const browserCode = typeof req.body?.browserCode === "string" ? req.body.browserCode : null; + router.get("/companies/:companyId/setup-token-login-sessions/:sessionId/prompt", async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; res.setHeader("Cache-Control", "no-store"); - // SR-6 and SR-7: the browser code is the confidential OAuth authorization - // secret. Enforce the fail-closed confidential transport guard before the - // route reads it, so the code never rides an untrusted transport. - if (!enforceSetupTokenTransport(req, res)) return; - if (!browserCode) { - // The route echoes no input; it returns fixed error text only (SR-1). - res.status(400).json({ error: "A browserCode is required." }); + // The full login URL is a confidential response. The route + // does not force TLS. It attaches a non-blocking advisory instead. + const transportAdvisory = assessSetupTokenTransport(req); + try { + const sessionId = req.params.sessionId as string; + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + const descriptor = setupTokenLoginService.describeOwned(sessionId, scope); + if (!descriptor.loginUrl) { + // The prompt has not surfaced yet. Return the same not-found error as a + // missing or a foreign session, so the route never confirms the session + // exists before the URL is ready. + res.status(404).json({ error: SETUP_TOKEN_SESSION_NOT_FOUND }); + return; + } + // The full login URL rides only in this authorized owner response. + const body: ClaudeSetupTokenSessionPrompt = { + authorizationUrl: descriptor.loginUrl, + transportAdvisory, + }; + res.json(body); + } catch (err) { + sendSetupTokenError(res, err); + } + }); + + router.post("/companies/:companyId/setup-token-login-sessions/:sessionId/code", async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; + res.setHeader("Cache-Control", "no-store"); + // The browser code is the confidential OAuth authorization + // secret. The route does not force TLS. It attaches a non-blocking advisory + // to the response instead, so the client can show a disclaimer. + const transportAdvisory = assessSetupTokenTransport(req); + // Parse the request with the shared strict validator before the route forwards + // the code to the live process. `.strict()` rejects an unknown field, and the + // grammar rejects an empty, an oversized, or a control-byte code. The route + // echoes no input; it returns fixed error text only. + const parsed = submitBrowserCodeRequestSchema.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: "A valid browser code is required." }); return; } try { - const result = setupTokenLoginService.submitCode(req.params.sessionId as string, scope, browserCode); - res.json({ state: result.state }); + const sessionId = req.params.sessionId as string; + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + setupTokenLoginService.submitCode(sessionId, scope, parsed.data.browserCode); + const descriptor = setupTokenLoginService.describeOwned(sessionId, scope); + const body: ClaudeSetupTokenSessionResponse = { + ...toClaudePublicResponse(descriptor), + transportAdvisory, + }; + res.json(body); } catch (err) { sendSetupTokenError(res, err); } }); - router.post("/agents/:id/setup-token-login-sessions/:sessionId/cancel", async (req, res) => { - const agent = await resolveSetupTokenAgent(req, res); - if (!agent) return; - const scope = buildSetupTokenScope(req, agent); - try { - const result = await setupTokenLoginService.cancel(req.params.sessionId as string, scope); - res.json({ state: result.state }); - } catch (err) { - sendSetupTokenError(res, err); - } - }); - - router.post("/agents/:id/setup-token-login-sessions/:sessionId/expire", async (req, res) => { - const agent = await resolveSetupTokenAgent(req, res); - if (!agent) return; - const scope = buildSetupTokenScope(req, agent); - try { - const result = await setupTokenLoginService.expire(req.params.sessionId as string, scope); - res.json({ state: result.state }); - } catch (err) { - sendSetupTokenError(res, err); - } - }); - - router.post("/agents/:id/setup-token-login-sessions/:sessionId/token", async (req, res) => { - const agent = await resolveSetupTokenAgent(req, res); - if (!agent) return; - const scope = buildSetupTokenScope(req, agent); + router.post("/companies/:companyId/setup-token-login-sessions/:sessionId/completion", async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; res.setHeader("Cache-Control", "no-store"); - // SR-6 and SR-7: the token is a confidential response. - if (!enforceSetupTokenTransport(req, res)) return; try { - // The service returns the token one time from a completed session. It - // returns the fixed unavailable error when the token is not ready or the - // owner already received it. The full token rides only in this authorized - // owner response over the confidential transport (SR-5, SR-6, SR-7). - const result = setupTokenLoginService.receiveToken(req.params.sessionId as string, scope); - res.json({ token: result.token }); + const sessionId = req.params.sessionId as string; + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + // The service returns the non-secret `storedSessionId` claim from a + // completed session whose owner-bound secret write succeeded. The response + // carries no token. + const result = setupTokenLoginService.completeSession(sessionId, scope); + const body: ClaudeSetupTokenCompletionResponse = { storedSessionId: result.storedSessionId }; + res.json(body); } catch (err) { sendSetupTokenError(res, err); } }); + router.post("/companies/:companyId/setup-token-login-sessions/:sessionId/cancel", async (req, res) => { + const companyId = req.params.companyId as string; + const ownerUserId = resolveCompanySessionOwner(req, companyId, res); + if (ownerUserId === null) return; + res.setHeader("Cache-Control", "no-store"); + const sessionId = req.params.sessionId as string; + try { + const scope = setupTokenLoginService.resolveCompanyScope( + sessionId, + companySetupTokenKey(companyId, ownerUserId), + ); + await setupTokenLoginService.cancel(sessionId, scope); + res.status(200).json({}); + } catch (err) { + // Cancel is idempotent. The service removes a session when it reaches a + // terminal state, so a repeat cancel, a cancel after a timeout, or a + // cancel of an unknown session finds no record and throws the fixed + // not-found error. Return the same success as an active cancel, so the + // client stops the poll and returns to its start state. + // + // This keeps the not-found uniform. The 200 response is identical + // for a missing session, an already-terminal session, and a foreign + // session, so the route never confirms a session exists and cancels + // nothing for a foreign id. A non-member still fails closed with a 404 at + // the company-access gate above, before this handler runs. A non-404 + // error still surfaces. + if (err instanceof SetupTokenSessionError && err.status === 404) { + res.status(200).json({}); + return; + } + sendSetupTokenError(res, err); + } + }); + router.get("/companies/:companyId/heartbeat-runs", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); diff --git a/server/src/routes/environments.ts b/server/src/routes/environments.ts index 2590215ce9..834f1e2407 100644 --- a/server/src/routes/environments.ts +++ b/server/src/routes/environments.ts @@ -583,6 +583,12 @@ export function environmentRoutes( if (impact.staticReferences.isInstanceDefault) { return "Cannot delete the current instance default environment. Set a new default environment before deleting this one."; } + if (impact.pendingCleanupLeaseCount > 0) { + return "Cannot delete this environment while a sandbox cleanup is pending. Wait for the cleanup sweep to destroy the orphan sandbox, then retry."; + } + if (impact.reusableSandboxLeaseCount > 0) { + return "Cannot delete this environment while it has a reusable sandbox lease. Remove the associated execution workspace or issue so Paperclip can destroy the sandbox, then retry."; + } return null; } @@ -735,6 +741,7 @@ export function environmentRoutes( templateRefKind: driver.templateRefKind, templateConfigBinding: driver.templateConfigBinding, supportsTemplateDelete: driver.supportsTemplateDelete, + supportsSetupTokenLogin: driver.supportsSetupTokenLogin ?? false, displayName: driver.displayName, description: driver.description, source: "plugin" as const, @@ -1089,6 +1096,27 @@ export function environmentRoutes( }), }); assertNoClientPlatformProvisionedMarkers(req.body.metadata); + // The durable `pending_cleanup` lease row stores the provider, the provider + // lease id, and the immutable config metadata for an orphan sandbox. The + // teardown retry reads that row alone and never reads the current environment + // provider. So a provider change or an environment delete after the record + // lands cannot strand the teardown. That immutable record is the correctness + // invariant. + // + // This pre-transaction check is a best-effort fast-fail only. It rejects a + // provider change while a known `pending_cleanup` lease exists, so the + // operator resolves the cleanup first. An orphan record that lands after this + // check still carries its own immutable teardown context, so the + // time-of-check-to-time-of-use window here cannot strand a sandbox. + const changesProviderTarget = + (req.body.driver !== undefined && req.body.driver !== existing.driver) || + req.body.config !== undefined; + if (changesProviderTarget && (await svc.hasUnresolvedPendingCleanupLeases(existing.id))) { + throw conflict( + "Cannot change the driver or provider config while a sandbox cleanup is pending. Wait for the cleanup sweep to destroy the orphan sandbox, then retry.", + { code: "environment_pending_sandbox_cleanup" }, + ); + } const actor = getActorInfo(req); const nextDriver = req.body.driver ?? existing.driver; const nextName = req.body.name ?? existing.name; diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index c855ea3eb6..4ab091bc23 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -224,6 +224,13 @@ import { importMcpJsonSchema, toolPolicyTestRequestSchema, createToolMcpGatewaySchema, + startClaudeSetupTokenSessionRequestSchema, + submitBrowserCodeRequestSchema, + claudeSetupTokenSessionResponseSchema, + claudeSetupTokenSessionPromptSchema, + claudeSetupTokenSessionOwnerResponseSchema, + claudeSetupTokenCompletionResponseSchema, + claudeOAuthTokenStatusResponseSchema, } from "@paperclipai/shared"; import { COMPANY_IMPORT_TRANSFERS_API_PATH, @@ -385,6 +392,10 @@ function zodToOpenApiSchema(schema: z.ZodTypeAny): JsonSchema { } const jsonSchema: JsonSchema = { type: "object", properties }; if (required.length > 0) jsonSchema.required = required; + // A `.strict()` Zod object forbids an unknown key. Publish that constraint + // as `additionalProperties: false`, so a client, a gateway, or a handler + // that treats the contract as authoritative rejects an extra property too. + if (unwrapped._def.unknownKeys === "strict") jsonSchema.additionalProperties = false; return jsonSchema; } @@ -4498,18 +4509,44 @@ registry.registerPath({ responses: { 200: r.ok(), 401: r.unauthorized }, }); -// Setup-token login session routes. The owner user starts one session, reads the -// login prompt, submits the browser code, and receives the token. The prompt, -// code, and token responses require a confidential transport; the guard returns -// 403 when the transport is not confidential. +// Company-and-environment Claude setup-token login session routes. The owner +// user starts one session for one adapter in one environment. The scope carries +// no agent id, so a hire flow with no agent still starts one session. The owner +// reads the login prompt, submits the browser code from the browser, and reads +// the completion claim. The prompt, code, and completion responses require a +// confidential transport; the guard returns 403 when the transport is not +// confidential. No response carries a token; the completion returns the +// non-secret `storedSessionId` claim. +// +// The stored-token status read returns only the secret id and the latest version +// of the owner value; it returns no token. The server derives the owner from the +// authenticated actor. A missing or a foreign value returns the same fixed 404, +// so the read discloses no existence distinction. The response is `no-store`. +registry.registerPath({ + method: "get", + path: "/api/companies/{companyId}/claude-oauth-token-status", + tags: ["companies"], + summary: "Read the stored Claude OAuth token status for the authenticated owner", + request: { params: z.object({ companyId: z.string() }) }, + responses: { + 200: r.ok(claudeOAuthTokenStatusResponseSchema), + 401: r.unauthorized, + 403: r.forbidden, + 404: r.notFound, + }, +}); + registry.registerPath({ method: "post", - path: "/api/agents/{id}/setup-token-login-sessions", - tags: ["agents"], - summary: "Start a setup-token login session for an agent", - request: { params: z.object({ id: z.string() }) }, + path: "/api/companies/{companyId}/setup-token-login-sessions", + tags: ["companies"], + summary: "Start a company-and-environment Claude setup-token login session", + request: { + params: z.object({ companyId: z.string() }), + body: jsonBody(startClaudeSetupTokenSessionRequestSchema), + }, responses: { - 201: r.ok(), + 201: r.ok(claudeSetupTokenSessionOwnerResponseSchema), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, @@ -4520,12 +4557,26 @@ registry.registerPath({ registry.registerPath({ method: "get", - path: "/api/agents/{id}/setup-token-login-sessions/{sessionId}/prompt", - tags: ["agents"], - summary: "Read the login prompt for a setup-token login session", - request: { params: z.object({ id: z.string(), sessionId: z.string() }) }, + path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}", + tags: ["companies"], + summary: "Read the status of a Claude setup-token login session", + request: { params: z.object({ companyId: z.string(), sessionId: z.string() }) }, responses: { - 200: r.ok(), + 200: r.ok(claudeSetupTokenSessionResponseSchema), + 401: r.unauthorized, + 403: r.forbidden, + 404: r.notFound, + }, +}); + +registry.registerPath({ + method: "get", + path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}/prompt", + tags: ["companies"], + summary: "Read the login prompt for a Claude setup-token login session", + request: { params: z.object({ companyId: z.string(), sessionId: z.string() }) }, + responses: { + 200: r.ok(claudeSetupTokenSessionPromptSchema), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, @@ -4535,12 +4586,15 @@ registry.registerPath({ registry.registerPath({ method: "post", - path: "/api/agents/{id}/setup-token-login-sessions/{sessionId}/code", - tags: ["agents"], - summary: "Submit the browser code for a setup-token login session", - request: { params: z.object({ id: z.string(), sessionId: z.string() }) }, + path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}/code", + tags: ["companies"], + summary: "Submit the browser code for a Claude setup-token login session", + request: { + params: z.object({ companyId: z.string(), sessionId: z.string() }), + body: jsonBody(submitBrowserCodeRequestSchema), + }, responses: { - 200: r.ok(), + 200: r.ok(claudeSetupTokenSessionResponseSchema), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, @@ -4550,40 +4604,12 @@ registry.registerPath({ registry.registerPath({ method: "post", - path: "/api/agents/{id}/setup-token-login-sessions/{sessionId}/cancel", - tags: ["agents"], - summary: "Cancel a setup-token login session", - request: { params: z.object({ id: z.string(), sessionId: z.string() }) }, + path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}/completion", + tags: ["companies"], + summary: "Read the completion claim of a Claude setup-token login session", + request: { params: z.object({ companyId: z.string(), sessionId: z.string() }) }, responses: { - 200: r.ok(), - 401: r.unauthorized, - 403: r.forbidden, - 404: r.notFound, - }, -}); - -registry.registerPath({ - method: "post", - path: "/api/agents/{id}/setup-token-login-sessions/{sessionId}/expire", - tags: ["agents"], - summary: "Expire a setup-token login session", - request: { params: z.object({ id: z.string(), sessionId: z.string() }) }, - responses: { - 200: r.ok(), - 401: r.unauthorized, - 403: r.forbidden, - 404: r.notFound, - }, -}); - -registry.registerPath({ - method: "post", - path: "/api/agents/{id}/setup-token-login-sessions/{sessionId}/token", - tags: ["agents"], - summary: "Receive the token from a completed setup-token login session", - request: { params: z.object({ id: z.string(), sessionId: z.string() }) }, - responses: { - 200: r.ok(), + 200: r.ok(claudeSetupTokenCompletionResponseSchema), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, @@ -4591,6 +4617,25 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}/cancel", + tags: ["companies"], + summary: "Cancel a Claude setup-token login session", + // The 404 is reserved for the pre-scope non-member gate. The company-access + // gate runs before the cancel logic and returns a fixed 404 for a non-member. + // Cancel itself is idempotent and stays uniform: a same-company owner-scoped + // missing, terminal, or foreign session id all return 200. So the route never + // confirms a session exists for the owner. + request: { params: z.object({ companyId: z.string(), sessionId: z.string() }) }, + responses: { + 200: r.ok(), + 401: r.unauthorized, + 403: r.forbidden, + 404: r.notFound, + }, +}); + // ─── Issue interactions & tree ─────────────────────────────────────────────── registry.registerPath({ diff --git a/server/src/routes/setup-token-route.test.ts b/server/src/routes/setup-token-route.test.ts index 53f19d2d34..3a9e3fac98 100644 --- a/server/src/routes/setup-token-route.test.ts +++ b/server/src/routes/setup-token-route.test.ts @@ -1,18 +1,20 @@ // The wired setup-token login route. This test drives the full login path — -// start, read-prompt, submit-code, and receive-token — through the HTTP route -// with a fake transport. It proves the guarded session contract is the live -// login path and that the security controls hold end to end at the route level. +// start, read-prompt, submit-code, and completion — through the HTTP route with +// a fake transport. It proves the guarded session contract is the live login +// path and that the security controls hold end to end at the route level. // // The test covers these criteria (the full set is on the parent plan document): // * The full path returns the sign-in URL to the owner, accepts one code, and -// returns the minted token one time. +// returns the non-secret storedSessionId claim after the secret write, with +// no token. // * SR-1 and SR-5: the browser code, the authorization-URL query values, and // the token never reach the request log, an activity detail, the exception // metadata, or a non-owner response. The owner read-prompt response carries // the full URL with `Cache-Control: no-store`; every other response carries // the sanitized URL form only. -// * SR-6 and SR-7: the fail-closed confidential transport guard rejects a -// non-TLS request and a spoofed forwarded protocol through the wired route. +// * The route does not force TLS. On a non-confidential +// transport it proceeds and attaches a non-blocking advisory to the +// response, so the login completes and the client shows a disclaimer. import express from "express"; import { Writable } from "node:stream"; @@ -21,21 +23,25 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { SETUP_TOKEN_SESSION_NOT_FOUND, - SETUP_TOKEN_SUBMIT_CONFLICT, - SETUP_TOKEN_TRANSPORT_INSECURE, + SETUP_TOKEN_PROVIDER_UNSUPPORTED, type SetupTokenCleanupRecord, type SetupTokenCleanupStore, type SetupTokenLeaseManager, type SetupTokenLoginOutcome, type SetupTokenLoginProcessFactory, + type SetupTokenSecretWriter, } from "../services/setup-token-session.js"; +import { SETUP_TOKEN_TRANSPORT_ADVISORY_CODE } from "@paperclipai/shared"; // --- Test fixtures ----------------------------------------------------------- const COMPANY_ID = "company-1"; +const OTHER_COMPANY_ID = "company-2"; const OWNER_USER_ID = "owner-user-1"; const OTHER_USER_ID = "other-user-2"; const AGENT_ID = "11111111-1111-4111-8111-111111111111"; +const ENVIRONMENT_ID = "22222222-2222-4222-8222-222222222222"; +const OTHER_ENVIRONMENT_ID = "33333333-3333-4333-8333-333333333333"; // The distinctive secret values. Each holds a unique marker, so a substring // check proves the value never reached a sink. @@ -69,6 +75,18 @@ function expectNoSecret(text: string): void { // --- Service mocks ----------------------------------------------------------- const mockAgentService = vi.hoisted(() => ({ getById: vi.fn() })); +// The environment service the company-and-environment routes call to resolve the +// sandbox environment server-side. A test sets the resolved environment shape to +// prove the fail-closed environment guard. +const mockEnvironmentService = vi.hoisted(() => ({ + getById: vi.fn(), + listBoundCompanyIds: vi.fn(), +})); +// The provider-capability resolver the setup-token routes call to gate the login +// on the provider setup-token capability. The default resolves "daytona" as a +// capable provider. A test overrides it to prove an unsupported provider fails +// closed before a session row, a lease, or a pseudo-terminal. +const mockResolvePluginSandboxProviderDriverByKey = vi.hoisted(() => vi.fn()); const mockLogActivity = vi.hoisted(() => vi.fn()); const mockHeartbeatService = vi.hoisted(() => ({ wakeup: vi.fn() })); const mockIssueService = vi.hoisted(() => ({ getById: vi.fn(), getByIdentifier: vi.fn() })); @@ -81,10 +99,34 @@ const mockInstanceSettingsService = vi.hoisted(() => ({ const mockRunSecretRedactionRegistry = vi.hoisted(() => ({ redactForRun: vi.fn(async (_companyId: string, _runId: string, value: unknown) => value), })); +// The narrow secrets service the status route calls. The route reads only +// `readClaudeOAuthUserSecretStatus`; a test drives its result to prove the route +// derives the owner from the actor and discloses no existence distinction. +const mockSecretService = vi.hoisted(() => ({ + readClaudeOAuthUserSecretStatus: vi.fn(), +})); function registerModuleMocks(): void { vi.doMock("../routes/authz.js", async () => vi.importActual("../routes/authz.js")); + vi.doMock("../services/secrets.js", async () => { + const actual = await vi.importActual( + "../services/secrets.js", + ); + return { ...actual, secretService: () => mockSecretService }; + }); vi.doMock("../services/agents.js", () => ({ agentService: () => mockAgentService })); + vi.doMock("../services/environments.js", () => ({ + environmentService: () => mockEnvironmentService, + })); + vi.doMock("../services/plugin-environment-driver.js", async () => { + const actual = await vi.importActual( + "../services/plugin-environment-driver.js", + ); + return { + ...actual, + resolvePluginSandboxProviderDriverByKey: mockResolvePluginSandboxProviderDriverByKey, + }; + }); vi.doMock("../services/heartbeat.js", () => ({ heartbeatService: () => mockHeartbeatService })); vi.doMock("../services/instance-settings.js", () => ({ instanceSettingsService: () => mockInstanceSettingsService, @@ -140,39 +182,94 @@ function useOwner(userId: string = OWNER_USER_ID): void { }; } +// Installs a non-`local_implicit` company member. The actor is a signed-in +// board user with an explicit company allow-list, so `hasCompanyAccess` denies a +// company that is not on the list. A test uses this actor to prove the +// cross-company read returns the fixed not-found error, not a membership oracle. +function useCompanyMember(userId: string = OWNER_USER_ID, companyIds: string[] = [COMPANY_ID]): void { + currentActor = { + type: "board", + userId, + companyIds, + source: "session", + isInstanceAdmin: false, + memberships: companyIds.map((companyId) => ({ + companyId, + status: "active", + membershipRole: "admin", + })), + }; +} + // --- Fake transport ---------------------------------------------------------- interface TransportHandle { factory: SetupTokenLoginProcessFactory; leases: SetupTokenLeaseManager; store: SetupTokenCleanupStore; + completeCredential: SetupTokenSecretWriter; submittedCodes: string[]; + secretWrites: string[]; + // The overwrite capture the session scope carried on each secret write. A + // first-write login records null; a confirmed-overwrite login records the + // captured expected secret id and version. + overwriteCaptures: Array< + { expectedSecretId: string; expectedLatestVersion: number } | null + >; + // Every cleanup record the store persisted. A test asserts the route writes no + // empty environment scope, so a rejected environment never reaches the store. + records: SetupTokenCleanupRecord[]; + // The count of login-process (pseudo-terminal) starts. A test asserts an + // unsupported provider starts no pseudo-terminal. + factoryInvocations: { count: number }; } /** * Builds a fake login transport. The factory surfaces the sign-in URL at once. - * On submit it either completes the login and delivers the token, throws an - * internal error, or leaves the process pending. The lease manager and the store - * are in-memory and hold no secret. + * On submit it either completes the login and writes the credential, throws an + * internal error, or leaves the process pending. The lease manager, the store, + * and the secret writer are in-memory and hold no secret in a durable sink. */ function buildTransport(opts: { onSubmit?: "complete" | "throw" | "pending" } = {}): TransportHandle { const mode = opts.onSubmit ?? "complete"; const submittedCodes: string[] = []; + // The owner-bound secret writer records only the token it received in memory, + // so a test can prove the write ran without a durable secret sink. It also + // records the overwrite capture from the session scope, so a test proves the + // start route threads the capture into the immutable scope. + const secretWrites: string[] = []; + const overwriteCaptures: Array< + { expectedSecretId: string; expectedLatestVersion: number } | null + > = []; + const completeCredential: SetupTokenSecretWriter = async (input) => { + secretWrites.push(input.token); + overwriteCaptures.push(input.scope.confirmedOverwrite ?? null); + }; const rows = new Map(); + const records: SetupTokenCleanupRecord[] = []; const store: SetupTokenCleanupStore = { async record(record) { rows.set(record.sessionId, { ...record }); + records.push({ ...record }); }, - async markState(sessionId, state) { - const row = rows.get(sessionId); + async markState(identity, state) { + const row = rows.get(identity.sessionId); if (row) row.state = state; }, - async remove(sessionId) { - rows.delete(sessionId); + async remove(identity) { + rows.delete(identity.sessionId); }, async listReapable() { return []; }, + async consumeStoredClaim(identity) { + const row = rows.get(identity.sessionId); + if (!row || row.state !== "stored" || row.boundAt !== null || row.deadline <= Date.now()) { + return null; + } + row.boundAt = Date.now(); + return { ...row }; + }, }; const leases: SetupTokenLeaseManager = { async acquire() { @@ -181,7 +278,9 @@ function buildTransport(opts: { onSubmit?: "complete" | "throw" | "pending" } = async release() {}, async releaseById() {}, }; + const factoryInvocations = { count: 0 }; const factory: SetupTokenLoginProcessFactory = ({ onPrompt, onCredential }) => { + factoryInvocations.count += 1; onPrompt({ url: FULL_LOGIN_URL }); let resolveDone!: (outcome: SetupTokenLoginOutcome) => void; const done = new Promise((resolve) => { @@ -195,15 +294,29 @@ function buildTransport(opts: { onSubmit?: "complete" | "throw" | "pending" } = throw new Error("the sandbox pseudo-terminal write failed."); } if (mode === "complete") { - onCredential(MINTED_TOKEN); - resolveDone("success"); + // Await the credential sink (the owner-bound secret write) before the + // process reports success, the way the real runner awaits its sink. + void onCredential(MINTED_TOKEN).then( + () => resolveDone("success"), + () => resolveDone("failure"), + ); } // pending: the process stays open; the test drives no completion. }, stop() {}, }; }; - return { factory, leases, store, submittedCodes }; + return { + factory, + leases, + store, + completeCredential, + submittedCodes, + secretWrites, + overwriteCaptures, + records, + factoryInvocations, + }; } // --- App builder with a capturing request logger ----------------------------- @@ -289,7 +402,12 @@ async function createApp(opts: { deploymentMode: opts.deploymentMode, confidentialProxyAllowlist: opts.confidentialProxyAllowlist, setupTokenLogin: opts.transport - ? { factory: opts.transport.factory, leases: opts.transport.leases, store: opts.transport.store } + ? { + factory: opts.transport.factory, + leases: opts.transport.leases, + store: opts.transport.store, + completeCredential: opts.transport.completeCredential, + } : undefined, }), ); @@ -305,224 +423,619 @@ async function settle(): Promise { } } -const BASE = `/api/agents/${AGENT_ID}/setup-token-login-sessions`; - beforeEach(() => { vi.resetModules(); registerModuleMocks(); vi.clearAllMocks(); useOwner(); + // The default owner has no stored Claude value. A test overrides this to prove + // the status route returns the metadata for a present owner value. + mockSecretService.readClaudeOAuthUserSecretStatus.mockResolvedValue(null); mockAgentService.getById.mockResolvedValue({ id: AGENT_ID, companyId: COMPANY_ID, name: "Claude agent", adapterType: "claude_local", + defaultEnvironmentId: ENVIRONMENT_ID, }); + // The default resolved environment is an active sandbox on a provider that + // supports the setup-token login, so the start routes pass the fail-closed + // guard. A test overrides this to prove the guard rejects an invalid + // environment or an unsupported provider. + mockEnvironmentService.getById.mockResolvedValue({ + id: ENVIRONMENT_ID, + driver: "sandbox", + status: "active", + config: { provider: "daytona" }, + }); + // The default environment has no company binding, so it is instance-global + // and open to every member. A test overrides this to prove the guard rejects + // an environment that another company owns. + mockEnvironmentService.listBoundCompanyIds.mockResolvedValue([]); + // The default provider advertises the setup-token login capability. A test + // overrides this to prove an unsupported provider fails closed. + mockResolvePluginSandboxProviderDriverByKey.mockImplementation( + async ({ driverKey }: { driverKey: string }) => + driverKey === "daytona" + ? { plugin: { id: "plugin-daytona" }, driver: { supportsSetupTokenLogin: true } } + : null, + ); }); -describe("setup-token login route — full path", () => { - it("drives start, read-prompt, submit-code, and receive-token with a fake transport", async () => { +// --- Company-and-environment routes ------------------------------------------ +// +// These tests drive the agentless Claude login. The scope binds one login to one +// company, one owner, one adapter, and one environment. The route carries no +// agent id, resolves the environment server-side, and returns the same not-found +// error for a foreign session as for a missing session. + +const COMPANY_BASE = `/api/companies/${COMPANY_ID}/setup-token-login-sessions`; + +// Starts a company-and-environment login and returns the session id. The default +// body names the Claude adapter and the resolved sandbox environment. +async function startCompanySession( + app: express.Express, + body: Record = { environmentId: ENVIRONMENT_ID, adapterType: "claude_local" }, +): Promise { + return request(app).post(COMPANY_BASE).send(body); +} + +describe("company-and-environment setup-token route — full path", () => { + it("drives start, status, prompt, code, and completion with no agent id", async () => { const transport = buildTransport({ onSubmit: "complete" }); const { app } = await createApp({ transport }); - // Start the session. The route returns the opaque id and no secret. - const startRes = await request(app).post(BASE).send({}); + // Start. The response carries the environment, the panel mode, and no prompt + // and no secret. The full login URL rides only in the guarded prompt read. + const startRes = await startCompanySession(app); expect(startRes.status, JSON.stringify(startRes.body)).toBe(201); const sessionId = startRes.body.sessionId as string; expect(sessionId).toBeTruthy(); + expect(startRes.body.environmentId).toBe(ENVIRONMENT_ID); + expect(startRes.body.status).toBe("waiting_for_user"); + expect(startRes.body.panelMode).toBe("submitted_browser_code"); + expect(startRes.body.prompt).toBeNull(); + expect(startRes.body.failure).toBeNull(); expect(startRes.headers["cache-control"]).toBe("no-store"); + // The scope carries no agent id, so the response holds none. + expect(JSON.stringify(startRes.body)).not.toContain(AGENT_ID); expectNoSecret(JSON.stringify(startRes.body)); + // The store persisted the resolved environment, never an empty value. + expect(transport.records.map((r) => r.environmentId)).toEqual([ENVIRONMENT_ID]); + + // Read the public status. It carries no prompt and no secret. + const statusRes = await request(app).get(`${COMPANY_BASE}/${sessionId}`).send(); + expect(statusRes.status, JSON.stringify(statusRes.body)).toBe(200); + expect(statusRes.body.status).toBe("waiting_for_user"); + expect(statusRes.body.environmentId).toBe(ENVIRONMENT_ID); + expect(statusRes.body.prompt).toBeUndefined(); + expectNoSecret(JSON.stringify(statusRes.body)); + // Read the prompt. The owner response carries the full URL, with no-store. - const promptRes = await request(app).get(`${BASE}/${sessionId}/prompt`).send(); + const promptRes = await request(app).get(`${COMPANY_BASE}/${sessionId}/prompt`).send(); expect(promptRes.status, JSON.stringify(promptRes.body)).toBe(200); - expect(promptRes.body.loginUrl).toBe(FULL_LOGIN_URL); - expect(promptRes.body.state).toBe("awaiting_code"); + expect(promptRes.body.authorizationUrl).toBe(FULL_LOGIN_URL); expect(promptRes.headers["cache-control"]).toBe("no-store"); + // The default deployment is a local-trusted loopback: a confidential + // transport. So the prompt carries no advisory. + expect(promptRes.body.transportAdvisory).toBeNull(); // Submit the one browser code. The response carries no secret. - const codeRes = await request(app).post(`${BASE}/${sessionId}/code`).send({ browserCode: BROWSER_CODE }); + const codeRes = await request(app) + .post(`${COMPANY_BASE}/${sessionId}/code`) + .send({ browserCode: BROWSER_CODE }); expect(codeRes.status, JSON.stringify(codeRes.body)).toBe(200); + expect(codeRes.body.transportAdvisory).toBeNull(); expect(transport.submittedCodes).toEqual([BROWSER_CODE]); expectNoSecret(JSON.stringify(codeRes.body)); await settle(); + expect(transport.secretWrites).toEqual([MINTED_TOKEN]); - // Receive the token one time. The owner response carries the token, with - // no-store. - const tokenRes = await request(app).post(`${BASE}/${sessionId}/token`).send({}); - expect(tokenRes.status, JSON.stringify(tokenRes.body)).toBe(200); - expect(tokenRes.body.token).toBe(MINTED_TOKEN); - expect(tokenRes.headers["cache-control"]).toBe("no-store"); - - // The second receive returns the same not-found error as a missing session. - const secondTokenRes = await request(app).post(`${BASE}/${sessionId}/token`).send({}); - expect(secondTokenRes.status).toBe(404); - expect(secondTokenRes.body.error).toBe(SETUP_TOKEN_SESSION_NOT_FOUND); + // Read the completion. The owner response carries the non-secret + // storedSessionId and no token, with no-store. + const completionRes = await request(app).post(`${COMPANY_BASE}/${sessionId}/completion`).send(); + expect(completionRes.status, JSON.stringify(completionRes.body)).toBe(200); + expect(completionRes.body.storedSessionId).toBe(sessionId); + expect(completionRes.body.token).toBeUndefined(); + expect(completionRes.headers["cache-control"]).toBe("no-store"); + expectNoSecret(JSON.stringify(completionRes.body)); }); it("returns the fixed no-secret error when no transport is bound", async () => { const { app } = await createApp({}); - const startRes = await request(app).post(BASE).send({}); + const startRes = await startCompanySession(app); expect(startRes.status).toBe(503); expect(startRes.headers["cache-control"]).toBe("no-store"); expectNoSecret(JSON.stringify(startRes.body)); }); +}); - it("binds the session to the owner: a different owner gets the same not-found error", async () => { +describe("company-and-environment setup-token route — object-level authorization", () => { + it("returns the same not-found for a cross-owner caller on every action", async () => { const transport = buildTransport({ onSubmit: "pending" }); const { app } = await createApp({ transport }); - const startRes = await request(app).post(BASE).send({}); + const startRes = await startCompanySession(app); const sessionId = startRes.body.sessionId as string; - // A different owner reads the same id. The route returns the not-found error, - // so a caller cannot tell a cross-owner session from a missing one. + // A different owner reads the same id. Every read action returns the + // not-found error, so a caller cannot tell a cross-owner session from a + // missing one. useOwner(OTHER_USER_ID); - const promptRes = await request(app).get(`${BASE}/${sessionId}/prompt`).send(); - expect(promptRes.status).toBe(404); - expect(promptRes.body.error).toBe(SETUP_TOKEN_SESSION_NOT_FOUND); - expect(JSON.stringify(promptRes.body)).not.toContain(URL_CODE_QUERY); - }); -}); + const paths: Array<() => request.Test> = [ + () => request(app).get(`${COMPANY_BASE}/${sessionId}`).send(), + () => request(app).get(`${COMPANY_BASE}/${sessionId}/prompt`).send(), + () => request(app).post(`${COMPANY_BASE}/${sessionId}/code`).send({ browserCode: BROWSER_CODE }), + () => request(app).post(`${COMPANY_BASE}/${sessionId}/completion`).send(), + ]; + for (const call of paths) { + const res = await call(); + expect(res.status).toBe(404); + expect(res.body.error).toBe(SETUP_TOKEN_SESSION_NOT_FOUND); + expectNoSecret(JSON.stringify(res.body)); + } -describe("setup-token login route — SR-1 and SR-5 (no secret in a sink)", () => { - it("keeps the code, the URL query, and the token out of every log, activity, and non-owner sink", async () => { - const transport = buildTransport({ onSubmit: "complete" }); - const { app, logLines } = await createApp({ transport }); + // Cancel is idempotent, so a cross-owner cancel returns the same 200 success + // as a repeat cancel and a cancel of a missing session. The response is + // identical for a foreign, an already-terminal, and a missing session, so it + // is not an existence oracle. The route cancels nothing for a foreign id: the + // scope check throws before the cancel runs, so the session stays active. + const cancelRes = await request(app).post(`${COMPANY_BASE}/${sessionId}/cancel`).send(); + expect(cancelRes.status).toBe(200); + expectNoSecret(JSON.stringify(cancelRes.body)); - const startRes = await request(app).post(BASE).send({}); - const sessionId = startRes.body.sessionId as string; - - // The invalid-session path: a bogus id with the code in the body. - const invalidRes = await request(app) - .post(`${BASE}/bogus-session-id/code`) - .send({ browserCode: BROWSER_CODE }); - expect(invalidRes.status).toBe(404); - expect(invalidRes.body.error).toBe(SETUP_TOKEN_SESSION_NOT_FOUND); - expectNoSecret(JSON.stringify(invalidRes.body)); - - // The first submit completes the login. - const firstCode = await request(app).post(`${BASE}/${sessionId}/code`).send({ browserCode: BROWSER_CODE }); - expect(firstCode.status).toBe(200); - await settle(); - - // The duplicate-submit path: the same code again, after completion. - const duplicateRes = await request(app) - .post(`${BASE}/${sessionId}/code`) - .send({ browserCode: BROWSER_CODE }); - expect(duplicateRes.status).toBe(409); - expect(duplicateRes.body.error).toBe(SETUP_TOKEN_SUBMIT_CONFLICT); - expectNoSecret(JSON.stringify(duplicateRes.body)); - - await settle(); - - // The request log holds no secret, and it did log the redacted request body, - // so the check is not vacuous. - const logText = logLines.join("\n"); - expectNoSecret(logText); - expect(logText).toContain("[REDACTED]"); - - // No activity detail holds a secret. - const activityText = JSON.stringify(mockLogActivity.mock.calls); - expectNoSecret(activityText); - }); - - it("keeps the code out of the exception metadata on the internal-error path", async () => { - const transport = buildTransport({ onSubmit: "throw" }); - const { app, logLines } = await createApp({ transport }); - - const startRes = await request(app).post(BASE).send({}); - const sessionId = startRes.body.sessionId as string; - - const errorRes = await request(app).post(`${BASE}/${sessionId}/code`).send({ browserCode: BROWSER_CODE }); - expect(errorRes.status).toBe(500); - expect(errorRes.body.error).toBe("Internal server error"); - expectNoSecret(JSON.stringify(errorRes.body)); - - await settle(); - const logText = logLines.join("\n"); - expectNoSecret(logText); - }); -}); - -describe("setup-token login route — SR-6 and SR-7 (fail-closed transport guard)", () => { - it("rejects a non-TLS confidential request and a spoofed forwarded protocol", async () => { - // Authenticated mode with no proxy allowlist: a loopback HTTP peer does not - // pass the confidential guard, so read-prompt and receive-token fail closed. - const transport = buildTransport({ onSubmit: "complete" }); - const { app } = await createApp({ transport, deploymentMode: "authenticated", confidentialProxyAllowlist: [] }); - - const startRes = await request(app).post(BASE).send({}); - const sessionId = startRes.body.sessionId as string; - - // read-prompt over plain HTTP → fixed no-secret error, no URL. - const promptRes = await request(app).get(`${BASE}/${sessionId}/prompt`).send(); - expect(promptRes.status).toBe(403); - expect(promptRes.body.error).toBe(SETUP_TOKEN_TRANSPORT_INSECURE); - expect(promptRes.headers["cache-control"]).toBe("no-store"); - expect(promptRes.body.loginUrl).toBeUndefined(); - expectNoSecret(JSON.stringify(promptRes.body)); - - // A spoofed forwarded protocol does not unlock the response. The guard reads - // the raw socket, not the header, unless the peer is on the allowlist. - const spoofRes = await request(app) - .get(`${BASE}/${sessionId}/prompt`) - .set("X-Forwarded-Proto", "https") - .send(); - expect(spoofRes.status).toBe(403); - expect(spoofRes.body.error).toBe(SETUP_TOKEN_TRANSPORT_INSECURE); - expectNoSecret(JSON.stringify(spoofRes.body)); - - // submit-code over plain HTTP → the guard fails closed. The browser code is - // the confidential OAuth secret, so it must never ride an untrusted transport. - // A spoofed forwarded protocol does not unlock it either. The code never - // reaches the transport. - const codeRes = await request(app).post(`${BASE}/${sessionId}/code`).send({ browserCode: BROWSER_CODE }); - expect(codeRes.status).toBe(403); - expect(codeRes.body.error).toBe(SETUP_TOKEN_TRANSPORT_INSECURE); - expectNoSecret(JSON.stringify(codeRes.body)); - - const spoofCodeRes = await request(app) - .post(`${BASE}/${sessionId}/code`) - .set("X-Forwarded-Proto", "https") - .send({ browserCode: BROWSER_CODE }); - expect(spoofCodeRes.status).toBe(403); - expect(spoofCodeRes.body.error).toBe(SETUP_TOKEN_TRANSPORT_INSECURE); - expectNoSecret(JSON.stringify(spoofCodeRes.body)); + // The owner reads the session again. It is still active, so the cross-owner + // cancel did not terminate it. + useOwner(OWNER_USER_ID); + const ownerStatus = await request(app).get(`${COMPANY_BASE}/${sessionId}`).send(); + expect(ownerStatus.status).toBe(200); + expect(ownerStatus.body.status).toBe("waiting_for_user"); expect(transport.submittedCodes).toEqual([]); - - // receive-token over plain HTTP also fails closed and leaks no token. - const tokenRes = await request(app) - .post(`${BASE}/${sessionId}/token`) - .set("X-Forwarded-Proto", "https") - .send({}); - expect(tokenRes.status).toBe(403); - expect(tokenRes.body.error).toBe(SETUP_TOKEN_TRANSPORT_INSECURE); - expect(tokenRes.body.token).toBeUndefined(); - expectNoSecret(JSON.stringify(tokenRes.body)); }); - it("ignores a forwarded protocol from a non-allowlisted peer", async () => { - // A non-empty allowlist that does not match the loopback peer still fails - // closed for a forwarded HTTPS protocol. + it("returns the same not-found for a cross-company caller", async () => { const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + + const startRes = await startCompanySession(app); + const sessionId = startRes.body.sessionId as string; + + // The same owner reads the session under a different company path. The + // session belongs to the first company, so the read returns the not-found + // error and never confirms the session across a company boundary. + const crossCompanyBase = `/api/companies/${OTHER_COMPANY_ID}/setup-token-login-sessions`; + const res = await request(app).get(`${crossCompanyBase}/${sessionId}`).send(); + expect(res.status).toBe(404); + expect(res.body.error).toBe(SETUP_TOKEN_SESSION_NOT_FOUND); + }); + + it("returns the fixed not-found for a non-member on every action across a company boundary", async () => { + const transport = buildTransport({ onSubmit: "complete" }); + const { app } = await createApp({ transport }); + + // A member of one company starts the session in that company. + const startRes = await startCompanySession(app); + const sessionId = startRes.body.sessionId as string; + + // A signed-in board member of the first company reads the session under a + // second company it is not a member of. The company reference is a + // cross-company reference. The route must return the same fixed not-found + // error as a missing session, so it is not a company-membership oracle. It + // must not return a 403, a login URL, a token, or the browser code forward. + useCompanyMember(OWNER_USER_ID, [COMPANY_ID]); + const crossCompanyBase = `/api/companies/${OTHER_COMPANY_ID}/setup-token-login-sessions`; + const paths: Array<() => request.Test> = [ + () => request(app).get(`${crossCompanyBase}/${sessionId}`).send(), + () => request(app).get(`${crossCompanyBase}/${sessionId}/prompt`).send(), + () => request(app).post(`${crossCompanyBase}/${sessionId}/code`).send({ browserCode: BROWSER_CODE }), + () => request(app).post(`${crossCompanyBase}/${sessionId}/completion`).send(), + () => request(app).post(`${crossCompanyBase}/${sessionId}/cancel`).send(), + ]; + for (const call of paths) { + const res = await call(); + expect(res.status).toBe(404); + expect(res.body.error).toBe(SETUP_TOKEN_SESSION_NOT_FOUND); + expect(res.body.authorizationUrl).toBeUndefined(); + expect(res.body.loginUrl).toBeUndefined(); + expect(res.body.token).toBeUndefined(); + expect(res.body.storedSessionId).toBeUndefined(); + expectNoSecret(JSON.stringify(res.body)); + } + // The code route returned the not-found error before it forwarded the code, + // so the login process received no submit. + expect(transport.submittedCodes).toEqual([]); + }); + + it("rejects an adapter other than claude_local at start", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + + const res = await startCompanySession(app, { + environmentId: ENVIRONMENT_ID, + adapterType: "codex_local", + }); + expect(res.status).toBe(400); + // The route rejected the adapter before it started a session, so the store + // holds no record. + expect(transport.records).toEqual([]); + }); +}); + +describe("company-and-environment setup-token route — idempotent cancel", () => { + it("cancels an active session, then returns 200 on a repeat cancel", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + + const startRes = await startCompanySession(app); + const sessionId = startRes.body.sessionId as string; + + // The first cancel terminates the active session and returns success. + const firstCancel = await request(app).post(`${COMPANY_BASE}/${sessionId}/cancel`).send(); + expect(firstCancel.status).toBe(200); + expect(firstCancel.headers["cache-control"]).toBe("no-store"); + await settle(); + + // The server removed the terminal session, so the status read now returns the + // not-found error. This is the state the panel used to poll forever. + const statusRes = await request(app).get(`${COMPANY_BASE}/${sessionId}`).send(); + expect(statusRes.status).toBe(404); + + // The repeat cancel finds no record. It returns the same 200 success instead + // of a hard 404, so the client can stop the poll. + const repeatCancel = await request(app).post(`${COMPANY_BASE}/${sessionId}/cancel`).send(); + expect(repeatCancel.status).toBe(200); + expect(repeatCancel.headers["cache-control"]).toBe("no-store"); + expectNoSecret(JSON.stringify(repeatCancel.body)); + }); + + it("returns 200 for a cancel of an unknown session id", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + + // The owner is a member of the company, but the session id never existed. The + // cancel is idempotent, so it returns success, not a 404. + const res = await request(app) + .post(`${COMPANY_BASE}/00000000-0000-4000-8000-000000000000/cancel`) + .send(); + expect(res.status).toBe(200); + expect(res.headers["cache-control"]).toBe("no-store"); + expectNoSecret(JSON.stringify(res.body)); + }); +}); + +describe("company-and-environment setup-token route — fail-closed environment", () => { + it("rejects a missing environment and writes no scope", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + mockEnvironmentService.getById.mockResolvedValue(null); + + const res = await startCompanySession(app); + expect(res.status).toBe(422); + // The route rejected the environment before it started a session, so no + // store holds an empty environment scope. + expect(transport.records).toEqual([]); + }); + + it("rejects an absent environment id in the request body", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + + const res = await startCompanySession(app, { adapterType: "claude_local" }); + expect(res.status).toBe(400); + expect(transport.records).toEqual([]); + }); + + it("rejects an archived, a local, an ssh, and a fake-provider environment", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + + const cases = [ + { id: ENVIRONMENT_ID, driver: "sandbox", status: "archived", config: { provider: "kubernetes" } }, + { id: ENVIRONMENT_ID, driver: "local", status: "active", config: {} }, + { id: ENVIRONMENT_ID, driver: "ssh", status: "active", config: {} }, + { id: ENVIRONMENT_ID, driver: "sandbox", status: "active", config: { provider: "fake" } }, + ]; + for (const environment of cases) { + mockEnvironmentService.getById.mockResolvedValueOnce(environment); + const res = await startCompanySession(app); + expect(res.status, JSON.stringify(environment)).toBe(422); + } + // No rejected environment reached the store. + expect(transport.records).toEqual([]); + }); + + it("rejects an environment that another company owns", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + // The environment binds to another company only, so the request company does + // not own it. The guard fails closed before the session starts. + mockEnvironmentService.listBoundCompanyIds.mockResolvedValue([OTHER_COMPANY_ID]); + + const res = await startCompanySession(app); + expect(res.status).toBe(403); + expect(res.body.error).toBe("The selected environment belongs to another company."); + // No rejected environment reached the store. + expect(transport.records).toEqual([]); + }); + + it("accepts an environment the request company also owns", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + // The environment binds to the request company and another company. A shared + // managed sandbox binds to many companies at once, so a co-owned binding + // stays open to the request company. + mockEnvironmentService.listBoundCompanyIds.mockResolvedValue([OTHER_COMPANY_ID, COMPANY_ID]); + + const res = await startCompanySession(app); + expect(res.status).toBe(201); + }); + + it("rejects a provider without the setup-token login capability and starts no session", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + // The resolved environment runs on a provider that does not advertise the + // setup-token login capability. The guard fails closed before a session row, + // a lease, or a pseudo-terminal. + mockEnvironmentService.getById.mockResolvedValue({ + id: ENVIRONMENT_ID, + driver: "sandbox", + status: "active", + config: { provider: "e2b" }, + }); + + const res = await startCompanySession(app); + expect(res.status, JSON.stringify(res.body)).toBe(422); + expect(res.body.error).toBe(SETUP_TOKEN_PROVIDER_UNSUPPORTED); + // No session row, no lease, and no pseudo-terminal started. + expect(transport.records).toEqual([]); + expect(transport.factoryInvocations.count).toBe(0); + expect(transport.submittedCodes).toEqual([]); + expect(transport.secretWrites).toEqual([]); + }); +}); + +describe("company-and-environment setup-token route — browser-code grammar", () => { + it("rejects a missing, an oversized, and a control-byte code before it forwards", async () => { + const transport = buildTransport({ onSubmit: "complete" }); + const { app } = await createApp({ transport }); + + const startRes = await startCompanySession(app); + const sessionId = startRes.body.sessionId as string; + + // A missing code. + const missing = await request(app).post(`${COMPANY_BASE}/${sessionId}/code`).send({}); + expect(missing.status).toBe(400); + + // An oversized code (one character over the bounded maximum). + const oversized = await request(app) + .post(`${COMPANY_BASE}/${sessionId}/code`) + .send({ browserCode: "a".repeat(513) }); + expect(oversized.status).toBe(400); + + // A control byte and a space are both rejected. + for (const badCode of ["abc\ndef", "abc def"]) { + const res = await request(app) + .post(`${COMPANY_BASE}/${sessionId}/code`) + .send({ browserCode: badCode }); + expect(res.status).toBe(400); + } + + // No malformed code reached the live login process. + expect(transport.submittedCodes).toEqual([]); + }); +}); + +describe("company-and-environment setup-token route — strict request contract", () => { + it("rejects a legacy ttlSeconds at start before any session or lease side effect", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + + const res = await startCompanySession(app, { + environmentId: ENVIRONMENT_ID, + adapterType: "claude_local", + ttlSeconds: 300, + }); + // The strict validator rejects the unknown field with a fixed 400. + expect(res.status).toBe(400); + // No session row, no lease, and no pseudo-terminal started. + expect(transport.records).toEqual([]); + expect(transport.factoryInvocations.count).toBe(0); + }); + + it("rejects an unknown field at start before any session or lease side effect", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + + const res = await startCompanySession(app, { + environmentId: ENVIRONMENT_ID, + adapterType: "claude_local", + agentId: "44444444-4444-4444-8444-444444444444", + }); + expect(res.status).toBe(400); + expect(transport.records).toEqual([]); + expect(transport.factoryInvocations.count).toBe(0); + }); + + it("rejects an unknown field on the browser-code route before it forwards", async () => { + const transport = buildTransport({ onSubmit: "complete" }); + const { app } = await createApp({ transport }); + + const startRes = await startCompanySession(app); + const sessionId = startRes.body.sessionId as string; + + const res = await request(app) + .post(`${COMPANY_BASE}/${sessionId}/code`) + .send({ browserCode: BROWSER_CODE, ttlSeconds: 300 }); + expect(res.status).toBe(400); + // The unknown field failed the strict parse before the code reached the + // live login process. + expect(transport.submittedCodes).toEqual([]); + }); +}); + +describe("company-and-environment setup-token route — advisory transport", () => { + it("proceeds on a non-TLS prompt and code request and attaches the advisory", async () => { + const transport = buildTransport({ onSubmit: "complete" }); const { app } = await createApp({ transport, deploymentMode: "authenticated", - confidentialProxyAllowlist: ["10.0.0.1"], + confidentialProxyAllowlist: [], }); - const startRes = await request(app).post(BASE).send({}); + const startRes = await startCompanySession(app); const sessionId = startRes.body.sessionId as string; - const promptRes = await request(app) - .get(`${BASE}/${sessionId}/prompt`) - .set("X-Forwarded-Proto", "https") - .send(); - expect(promptRes.status).toBe(403); - expect(promptRes.body.error).toBe(SETUP_TOKEN_TRANSPORT_INSECURE); - expectNoSecret(JSON.stringify(promptRes.body)); + // The prompt over plain HTTP surfaces the URL with the advisory. The URL + // query is the owner-only confidential value, so the body holds it by design. + const promptRes = await request(app).get(`${COMPANY_BASE}/${sessionId}/prompt`).send(); + expect(promptRes.status).toBe(200); + expect(promptRes.body.authorizationUrl).toBe(FULL_LOGIN_URL); + expect(promptRes.body.transportAdvisory).toEqual({ code: SETUP_TOKEN_TRANSPORT_ADVISORY_CODE }); + + // The code over plain HTTP proceeds. The code reaches the process and the + // response carries the advisory. + const codeRes = await request(app) + .post(`${COMPANY_BASE}/${sessionId}/code`) + .send({ browserCode: BROWSER_CODE }); + expect(codeRes.status).toBe(200); + expect(codeRes.body.transportAdvisory).toEqual({ code: SETUP_TOKEN_TRANSPORT_ADVISORY_CODE }); + expect(transport.submittedCodes).toEqual([BROWSER_CODE]); + expectNoSecret(JSON.stringify(codeRes.body)); + }); +}); + +// The stored-token status route and the overwrite capture are the two deltas of +// the apply-stored-token-first flow. The status route derives the owner from the +// actor and discloses no existence distinction. The start route threads the +// confirmed-overwrite capture into the immutable session scope, so the final +// secret write rotates the stored value instead of a first write. + +const STATUS_BASE = `/api/companies/${COMPANY_ID}/claude-oauth-token-status`; +const STORED_SECRET_ID = "44444444-4444-4444-8444-444444444444"; + +describe("company-and-environment stored-token status route", () => { + it("returns the owner metadata with no token and no-store, and never a token", async () => { + mockSecretService.readClaudeOAuthUserSecretStatus.mockResolvedValue({ + secretId: STORED_SECRET_ID, + latestVersion: 3, + }); + const { app } = await createApp({}); + + const res = await request(app).get(STATUS_BASE).send(); + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(res.body).toEqual({ secretId: STORED_SECRET_ID, latestVersion: 3 }); + expect(res.headers["cache-control"]).toBe("no-store"); + // The owner comes from the actor, never from the request. + expect(mockSecretService.readClaudeOAuthUserSecretStatus).toHaveBeenCalledWith( + COMPANY_ID, + OWNER_USER_ID, + ); + // The response carries no token. + expectNoSecret(JSON.stringify(res.body)); + }); + + it("returns the fixed not-found for an owner with no value", async () => { + mockSecretService.readClaudeOAuthUserSecretStatus.mockResolvedValue(null); + const { app } = await createApp({}); + + const res = await request(app).get(STATUS_BASE).send(); + expect(res.status).toBe(404); + expect(res.body.error).toBe(SETUP_TOKEN_SESSION_NOT_FOUND); + expect(res.headers["cache-control"]).toBe("no-store"); + }); + + it("gives a same-company other user no existence distinction", async () => { + // The route derives the owner from the actor. The service returns the value + // only for the real owner, so a same-company other user gets the same fixed + // not-found as an owner with no value. + mockSecretService.readClaudeOAuthUserSecretStatus.mockImplementation( + async (_companyId: string, ownerUserId: string) => + ownerUserId === OWNER_USER_ID ? { secretId: STORED_SECRET_ID, latestVersion: 1 } : null, + ); + const { app } = await createApp({}); + + const ownerRes = await request(app).get(STATUS_BASE).send(); + expect(ownerRes.status).toBe(200); + + // A different same-company user reads the same path. The route derives the + // owner as that user and gets the fixed not-found. + useOwner(OTHER_USER_ID); + const otherRes = await request(app).get(STATUS_BASE).send(); + expect(otherRes.status).toBe(404); + expect(otherRes.body.error).toBe(SETUP_TOKEN_SESSION_NOT_FOUND); + expect(otherRes.headers["cache-control"]).toBe("no-store"); + }); + + it("gives a cross-company caller the fixed not-found and never reads the value", async () => { + // A signed-in board user who is not a member of the company. The company + // gate returns the fixed not-found before any owner-value read runs. + useCompanyMember(OWNER_USER_ID, [OTHER_COMPANY_ID]); + const { app } = await createApp({}); + + const res = await request(app).get(STATUS_BASE).send(); + expect(res.status).toBe(404); + expect(res.body.error).toBe(SETUP_TOKEN_SESSION_NOT_FOUND); + expect(res.headers["cache-control"]).toBe("no-store"); + // The route never reads the owner value for a non-member. + expect(mockSecretService.readClaudeOAuthUserSecretStatus).not.toHaveBeenCalled(); + }); +}); + +describe("company-and-environment login overwrite capture", () => { + it("threads the confirmed-overwrite capture into the session scope", async () => { + const transport = buildTransport({ onSubmit: "complete" }); + const { app } = await createApp({ transport }); + + const startRes = await startCompanySession(app, { + environmentId: ENVIRONMENT_ID, + adapterType: "claude_local", + overwrite: { expectedSecretId: STORED_SECRET_ID, expectedLatestVersion: 2 }, + }); + expect(startRes.status, JSON.stringify(startRes.body)).toBe(201); + const sessionId = startRes.body.sessionId as string; + + await request(app) + .post(`${COMPANY_BASE}/${sessionId}/code`) + .send({ browserCode: BROWSER_CODE }); + await settle(); + + // The secret write ran with the overwrite capture from the session scope, so + // the writer selects a confirmed rotation instead of a first write. + expect(transport.secretWrites).toEqual([MINTED_TOKEN]); + expect(transport.overwriteCaptures).toEqual([ + { expectedSecretId: STORED_SECRET_ID, expectedLatestVersion: 2 }, + ]); + }); + + it("carries no overwrite capture for a plain first-write login", async () => { + const transport = buildTransport({ onSubmit: "complete" }); + const { app } = await createApp({ transport }); + + const startRes = await startCompanySession(app); + const sessionId = startRes.body.sessionId as string; + await request(app) + .post(`${COMPANY_BASE}/${sessionId}/code`) + .send({ browserCode: BROWSER_CODE }); + await settle(); + + expect(transport.overwriteCaptures).toEqual([null]); + }); + + it("rejects a malformed overwrite capture with a fixed 400", async () => { + const transport = buildTransport({ onSubmit: "pending" }); + const { app } = await createApp({ transport }); + + const res = await startCompanySession(app, { + environmentId: ENVIRONMENT_ID, + adapterType: "claude_local", + overwrite: { expectedSecretId: "not-a-uuid", expectedLatestVersion: 0 }, + }); + expect(res.status).toBe(400); + // The malformed-capture 400 is a request-validation error like the adapter + // and the environment checks; it carries no secret in the response body. + expectNoSecret(JSON.stringify(res.body)); }); }); diff --git a/server/src/services/agents.ts b/server/src/services/agents.ts index 3dcb016794..8226aac51b 100644 --- a/server/src/services/agents.ts +++ b/server/src/services/agents.ts @@ -29,7 +29,14 @@ import { conflict, notFound, unprocessable } from "../errors.js"; import { syncAgentAdapterEnvBindings } from "./agent-secret-bindings.js"; import { normalizeAgentPermissions } from "./agent-permissions.js"; import { REDACTED_EVENT_VALUE, sanitizeRecord } from "../redaction.js"; -import { secretService } from "./secrets.js"; +import { + assertClaudeOAuthBindingInvariant, + claudeOAuthClaimRejectedError, + CLAUDE_LOCAL_ADAPTER_TYPE, + secretService, + type ClaudeOAuthBindingInvariantDecision, +} from "./secrets.js"; +import { createDbSetupTokenCleanupStore } from "./setup-token-session.js"; import { builtInAgentMarkersEqual, readBuiltInAgentMarker, @@ -69,14 +76,36 @@ interface RevisionMetadata { rolledBackFromRevisionId?: string | null; } +/** + * The Claude login context for an agent write. The route derives the owner user + * from the authenticated actor, not from the request body, and forwards the + * non-secret `storedSessionId` claim from a completed Claude login session. A + * controlled internal override permits a migration or an administrator repair to + * bind or unbind the fixed OAuth token without a claim. + * + * The `applyExistingWithoutClaim` field is the user-actor apply-existing path. + * The route sets it only for an authenticated user actor and derives the owner + * from that actor. The path binds the fixed reference to the owner stored value + * with no login round trip. It is distinct from `allowInternalBindingOverride`, + * which does no ownership check. + */ +interface ClaudeLoginContext { + storedSessionId?: string | null; + ownerUserId?: string | null; + allowInternalBindingOverride?: boolean; + applyExistingWithoutClaim?: boolean; +} + interface UpdateAgentOptions { recordRevision?: RevisionMetadata; allowBuiltInAgentMetadata?: boolean; allowPendingApprovalConfigUpdate?: boolean; + claudeLogin?: ClaudeLoginContext; } interface CreateAgentOptions { allowBuiltInAgentMetadata?: boolean; + claudeLogin?: ClaudeLoginContext; } interface AgentShortnameRow { @@ -447,6 +476,83 @@ export function agentService(db: Db) { }); } + /** + * Enforces the Claude OAuth binding claim inside a write transaction. It runs + * after {@link assertClaudeOAuthBindingInvariant} decided that the write + * introduces or keeps the fixed binding. + * + * When the write introduces the fixed binding: + * * A create or hire path (`consume: true`) consumes a stored-session claim + * with one conditional write. It builds the claim scope from the company, + * the owner user, the fixed adapter, the environment, and the + * `storedSessionId`. It inserts the binding only when the write returns one + * row; otherwise it raises the fixed claim error, which rolls back the + * whole transaction and inserts no binding. + * * An update, approval, or rollback path (`consume: false`) carries no + * claim, so it raises the same fixed claim error at once. + * + * The user-actor apply-existing path (`applyExistingWithoutClaim`) binds the + * fixed reference with no login round trip. The route sets the flag only for + * an authenticated user actor and derives the owner from that actor. The gate + * permits the no-claim bind only when the owner already has a stored value for + * the company. It reads the owner value status; it reads no token. A missing + * owner or a missing stored value raises the same fixed claim error, so the + * caller cannot tell the reasons apart. + * + * A controlled internal override skips the claim for a migration or an + * administrator repair. The function creates the fixed user-secret definition + * before the caller runs the declaration synchronization, so the synchronized + * declaration always references an existing definition. + */ + async function enforceClaudeOAuthBindingClaim( + txDb: Db, + input: { + companyId: string; + decision: ClaudeOAuthBindingInvariantDecision; + consume: boolean; + environmentId: string | null; + claudeLogin?: ClaudeLoginContext; + }, + ): Promise { + const ownerUserId = input.claudeLogin?.ownerUserId ?? null; + if (input.decision.introducesBinding && !input.claudeLogin?.allowInternalBindingOverride) { + if (input.claudeLogin?.applyExistingWithoutClaim) { + // The user-actor apply-existing path. The route derived the owner from + // the authenticated user actor. The gate binds the fixed reference only + // when that owner already has a stored value. It reads no token. + if (!ownerUserId) { + throw claudeOAuthClaimRejectedError(); + } + const stored = await secretService(txDb).readClaudeOAuthUserSecretStatus( + input.companyId, + ownerUserId, + ); + if (!stored) { + throw claudeOAuthClaimRejectedError(); + } + } else if (!input.consume) { + throw claudeOAuthClaimRejectedError(); + } else { + const consumed = await createDbSetupTokenCleanupStore(txDb).consumeStoredClaim({ + sessionId: input.claudeLogin?.storedSessionId ?? "", + companyId: input.companyId, + ownerUserId: ownerUserId ?? "", + adapterType: CLAUDE_LOCAL_ADAPTER_TYPE, + environmentId: input.environmentId ?? "", + }); + if (!consumed) { + throw claudeOAuthClaimRejectedError(); + } + } + } + if (input.decision.introducesBinding || input.decision.keepsBinding) { + // Create the fixed definition before declaration synchronization. + await secretService(txDb).ensureClaudeOAuthUserSecretDefinition(input.companyId, { + userId: ownerUserId, + }); + } + } + function assertBuiltInAgentMetadataMutationAllowed( beforeMetadata: unknown, afterMetadata: unknown, @@ -526,6 +632,17 @@ export function agentService(db: Db) { { adapterType: (normalizedPatch.adapterType ?? existing.adapterType) as string }, ); } + // Run the server-enforced binding invariant when the patch touches the + // adapter config. The update, approval, and rollback paths keep an existing + // fixed binding but reject a newly introduced binding, because they carry no + // stored-session claim. + const bindingDecision = Object.prototype.hasOwnProperty.call(normalizedPatch, "adapterConfig") + ? assertClaudeOAuthBindingInvariant({ + adapterType: (normalizedPatch.adapterType ?? existing.adapterType) as string, + nextConfig: normalizedPatch.adapterConfig, + priorConfig: existing.adapterConfig, + }) + : null; const shouldRecordRevision = Boolean(options?.recordRevision) && hasConfigPatchFields(normalizedPatch); const beforeConfig = shouldRecordRevision ? buildConfigSnapshot(existing) : null; @@ -541,6 +658,15 @@ export function agentService(db: Db) { if (!updated) return null; if (Object.prototype.hasOwnProperty.call(normalizedPatch, "adapterConfig")) { + if (bindingDecision) { + await enforceClaudeOAuthBindingClaim(txDb, { + companyId: existing.companyId, + decision: bindingDecision, + consume: false, + environmentId: null, + claudeLogin: options?.claudeLogin, + }); + } await syncAgentSecretBindings(updated, txDb); } @@ -612,8 +738,25 @@ export function agentService(db: Db) { const adapterConfig = isPlainRecord(data.adapterConfig) ? await secretsSvc.normalizeAdapterConfigForPersistence(companyId, data.adapterConfig, { adapterType }) : {}; + // Run the server-enforced binding invariant after generic normalization + // and before any database write. A create has no prior config. + const bindingDecision = assertClaudeOAuthBindingInvariant({ + adapterType, + nextConfig: adapterConfig, + priorConfig: null, + }); return db.transaction(async (tx) => { const txDb = tx as unknown as Db; + // Consume the stored-session claim and create the fixed definition inside + // the same transaction that inserts the binding. A rejected claim rolls + // back the whole transaction and inserts no binding. + await enforceClaudeOAuthBindingClaim(txDb, { + companyId, + decision: bindingDecision, + consume: true, + environmentId: (data.defaultEnvironmentId as string | null | undefined) ?? null, + claudeLogin: options?.claudeLogin, + }); const created = await tx .insert(agents) .values({ @@ -790,6 +933,7 @@ export function agentService(db: Db) { if (!existing || existing.status !== "pending_approval") return null; const approvedPatch = approvedPayload ? configPatchFromApprovalPayload(approvedPayload) : {}; let patch = { ...approvedPatch } as Partial; + let approvalBindingDecision: ClaudeOAuthBindingInvariantDecision | null = null; if ( Object.prototype.hasOwnProperty.call(patch, "adapterConfig") && isPlainRecord(patch.adapterConfig) @@ -799,6 +943,13 @@ export function agentService(db: Db) { patch.adapterConfig, { adapterType: (patch.adapterType ?? existing.adapterType) as string }, ); + // The approval activation keeps an existing fixed binding but rejects a + // newly introduced binding, because it carries no stored-session claim. + approvalBindingDecision = assertClaudeOAuthBindingInvariant({ + adapterType: (patch.adapterType ?? existing.adapterType) as string, + nextConfig: patch.adapterConfig, + priorConfig: existing.adapterConfig, + }); } if (patch.permissions !== undefined) { patch.permissions = normalizeAgentPermissions( @@ -813,6 +964,14 @@ export function agentService(db: Db) { .returning() .then((rows) => rows[0] ?? null); if (!updated) return null; + if (approvalBindingDecision) { + await enforceClaudeOAuthBindingClaim(txDb, { + companyId: existing.companyId, + decision: approvalBindingDecision, + consume: false, + environmentId: null, + }); + } await syncAgentSecretBindings(updated, txDb); const agent = await agentService(txDb).getById(updated.id); if (!agent) { diff --git a/server/src/services/bundled-plugins.ts b/server/src/services/bundled-plugins.ts index a3c27a7e26..6e79c27b7d 100644 --- a/server/src/services/bundled-plugins.ts +++ b/server/src/services/bundled-plugins.ts @@ -1,5 +1,6 @@ import path from "node:path"; import fs from "node:fs"; +import type { PaperclipPluginManifestV1 } from "@paperclipai/shared"; /** * Bundled plugin auto-provisioning. @@ -190,16 +191,23 @@ interface RegistryPluginRow { id: string; pluginKey: string; status: string; + version: string; + manifestJson: PaperclipPluginManifestV1; } export interface BundledPluginProvisionerDeps { registry: { getByKey(pluginKey: string): Promise; + update( + id: string, + data: { version?: string; manifest?: PaperclipPluginManifestV1 }, + ): Promise; }; loader: { installPlugin(options: { localPath: string }): Promise<{ manifest: { id: string } | null; }>; + loadManifest(packagePath: string): Promise; }; lifecycle: { load(pluginId: string): Promise; @@ -216,6 +224,49 @@ function defaultBundleManifestExists(localPath: string): boolean { return fs.existsSync(path.join(localPath, "dist", "manifest.js")); } +/** + * Reconcile a present bundled plugin's persisted manifest with the shipped + * bundle. The bundle is part of the release image, so its manifest is the + * source of truth. When the bundle declares a version that differs from the + * persisted version, update the stored manifest and version. This propagates + * a manifest change (for example a new driver capability) to an existing + * install that the auto-install path skips. + * + * The step is fail-safe. A missing bundle, a manifest read error, or a + * database error is caught, logged, and swallowed, so boot always completes. + * The step updates only the stored manifest row; it never restarts the worker. + */ +async function reconcileBundledPluginManifest( + existing: RegistryPluginRow, + install: ResolvedBundledPlugin, + deps: BundledPluginProvisionerDeps, + bundleManifestExists: (localPath: string) => boolean, +): Promise { + try { + if (!bundleManifestExists(install.localPath)) return; + const bundleManifest = await deps.loader.loadManifest(install.localPath); + if (!bundleManifest) return; + if (bundleManifest.version === existing.version) return; + await deps.registry.update(existing.id, { + version: bundleManifest.version, + manifest: bundleManifest, + }); + deps.logger.info( + { + pluginKey: install.pluginKey, + fromVersion: existing.version, + toVersion: bundleManifest.version, + }, + "reconciled bundled plugin manifest to the shipped bundle version", + ); + } catch (err) { + deps.logger.error( + { err, pluginKey: install.pluginKey }, + "Failed to reconcile bundled plugin manifest; continuing boot with the stored manifest", + ); + } +} + /** * Ensure each resolved bundled plugin is installed and loaded. * @@ -226,7 +277,9 @@ function defaultBundleManifestExists(localPath: string): boolean { * * Skip semantics: * - A plugin present in any non-uninstalled state is skipped, so an - * operator-disabled plugin is not silently re-enabled on reboot. + * operator-disabled plugin is not silently re-enabled on reboot. Before the + * skip, the persisted manifest is reconciled to the shipped bundle version + * (see `reconcileBundledPluginManifest`). * - A soft-uninstalled plugin is reinstalled only when * `reinstallUninstalled` is set (managed mode, where the control plane * owns provisioning). Self-hosted keeps the pre-refactor behavior of @@ -242,6 +295,14 @@ export async function ensureBundledPlugins( try { const existing = await deps.registry.getByKey(install.pluginKey); if (existing && (existing.status !== "uninstalled" || !opts.reinstallUninstalled)) { + // The bundle ships with the release image, so its manifest is the + // source of truth for a present plugin. Reconcile the persisted + // manifest when the shipped bundle declares a newer version. Without + // this step a manifest capability added to a bundle never reaches an + // existing install, because the auto-install below skips a present + // plugin. The reconcile updates only the stored manifest row; the + // running worker already runs the shipped code. + await reconcileBundledPluginManifest(existing, install, deps, bundleManifestExists); deps.logger.info( { pluginKey: install.pluginKey, status: existing.status }, "bundled plugin already present; skipping auto-install", diff --git a/server/src/services/environment-config.ts b/server/src/services/environment-config.ts index 6cb113ed3f..7ebb31a506 100644 --- a/server/src/services/environment-config.ts +++ b/server/src/services/environment-config.ts @@ -714,6 +714,45 @@ export async function resolveEnvironmentDriverConfigForRuntime( return parsed; } +/** + * Resolve the connection secrets of a recorded sandbox config for a durable + * orphan-sandbox teardown. The retry reads the recorded config from the durable + * `pending_cleanup` lease row, not from the current environment. So this + * resolver must not require the environment binding: a delete removed the + * environment, or a provider change replaced the binding. It resolves each + * schema-declared secret ref by id at the latest version through + * `resolveSecretValueForSandboxCleanup`, which authorizes the read from the + * durable orphan record and skips the binding check. The resolved values are + * used once for the teardown RPC and never persisted. + */ +export async function resolveSandboxCleanupConfigSecrets( + db: Db, + companyId: string, + config: SandboxEnvironmentConfig, + context?: { issueId?: string | null; heartbeatRunId?: string | null }, +): Promise { + if (config.provider === "fake") return config; + const schema = await getSandboxProviderConfigSchema(db, config.provider); + const secrets = secretService(db); + let nextConfig = { ...config } as Record; + for (const path of collectSecretRefPaths(schema)) { + const current = canonicalizeSecretRefValue(readConfigValueAtPath(nextConfig, path), path); + if (typeof current !== "string") continue; + const trimmed = current.trim(); + if (!isUuidSecretRef(trimmed)) continue; + nextConfig = writeConfigValueAtPath( + nextConfig, + path, + await secrets.resolveSecretValueForSandboxCleanup(companyId, trimmed, "latest", { + configPath: path, + issueId: context?.issueId ?? null, + heartbeatRunId: context?.heartbeatRunId ?? null, + }), + ); + } + return nextConfig as SandboxEnvironmentConfig; +} + export function readSshEnvironmentPrivateKeySecretId( environment: Pick, ): string | null { diff --git a/server/src/services/environment-runtime.ts b/server/src/services/environment-runtime.ts index 9e5ecf9372..117e071f47 100644 --- a/server/src/services/environment-runtime.ts +++ b/server/src/services/environment-runtime.ts @@ -33,6 +33,7 @@ import { collectEnvironmentSecretRefs, parseEnvironmentDriverConfig, resolveEnvironmentDriverConfigForRuntime, + resolveSandboxCleanupConfigSecrets, stripSandboxProviderEnvelope, } from "./environment-config.js"; import { @@ -64,6 +65,18 @@ import { } from "./plugin-environment-driver.js"; import { collectSecretRefPaths } from "./json-schema-secret-refs.js"; import { buildWorkspaceRealizationRecordFromDriverInput } from "./workspace-realization.js"; +import { + createSandboxOrphanCleanupSpool, + type DeferredOrphanCleanupRecord, + type SandboxOrphanCleanupSpool, +} from "./sandbox-orphan-cleanup-spool.js"; +import { logger } from "../middleware/logger.js"; + +// The constant error kind for the durable orphan-cleanup-write-failed log. The +// log never reads the caught exception, because the exception can carry a +// credential in its name, code, message, cause, or stack. So the log records +// this constant and the plain provider identifiers only. +const SANDBOX_ORPHAN_CLEANUP_WRITE_ERROR_KIND = "sandbox_orphan_cleanup_write_failed"; // --------------------------------------------------------------------------- // Sandbox capability contract — one normalizer for both branches @@ -347,6 +360,26 @@ export interface EnvironmentDriverAcquireInput { * of the base image, matching what real agent runs do. */ applyCustomImageTemplate?: boolean; + /** + * The latest time the acquired lease may stay active. A caller with an + * independent deadline (for example the setup-token login session) sets it, + * so the driver bounds the persisted lease expiry to this time. The driver + * records the earlier of this time and the provider expiry. Null or undefined + * keeps the provider expiry only, so all other callers keep the current + * behavior. + */ + requestedExpiresAt?: Date | null; + /** + * Re-check the environment company binding inside the lease insert + * transaction. The login acquire paths set this so a managed reconciliation + * that binds the sandbox to another company between the route guard and this + * acquire cannot let the login run in a foreign-company sandbox. The lease + * insert then rejects a foreign-company environment with the 403 + * `environment_company_mismatch` and holds no lease. An unbound + * (instance-global) environment stays open. Other callers keep the current + * behavior. + */ + assertCompanyBinding?: boolean; } export interface EnvironmentDriverReleaseInput { @@ -450,6 +483,73 @@ export interface EnvironmentRuntimeDriver { * Only the sandbox driver implements it; other drivers omit it. */ effectiveSandboxCapabilities?(input: EnvironmentDriverLeaseInput): Promise; + /** + * Retry the provider teardown for an orphan sandbox that an earlier acquire + * provisioned but could not tear down. The pending-cleanup lease row carries + * the provider, the provider lease id, and the immutable config metadata, so + * the retry resolves the teardown from the row alone. It never reads the + * current environment provider, so a provider change or an environment delete + * cannot strand the teardown. `environment` is null when a delete already + * removed the environment row. The method throws when the teardown fails, so + * the cleanup sweep keeps the row for a later retry. + */ + retryPendingSandboxTeardown?(input: { environment: Environment | null; lease: EnvironmentLease }): Promise; + /** + * Report whether the provider worker can run an orphan teardown now. A plugin + * sandbox provider worker can be briefly down during its own restart window. + * A teardown in that window throws, and the cleanup sweep would count that + * throw against the finite retry cap. So the sweep probes this method before + * it claims a retry attempt, and skips the lease when the worker is not ready. + * A later sweep retries after the worker recovers. The method reports `true` + * for every persistent condition (a built-in provider, a missing plugin, or an + * absent worker manager), so the teardown still runs, throws, and counts + * toward the cap. It reports `false` only for the transient worker-down window. + */ + isPendingCleanupWorkerReady?(input: { environment: Environment | null; lease: EnvironmentLease }): Promise; + /** + * Flush the in-process buffer of orphan pending-cleanup records that every + * synchronous database write could not land. The acquire buffers an orphan + * here when the database is down after a failed teardown, so no durable row + * exists yet. The cleanup sweep calls this method each tick. The method + * re-inserts each buffered record. A record whose write now succeeds leaves the + * buffer and becomes a normal `pending_cleanup` row that the sweep tears down. + * A record whose write still fails stays in the buffer for a later flush. The + * method reports how many records it recovered and how many still wait. + */ + flushDeferredOrphanCleanups?(): Promise<{ recovered: number; pending: number }>; +} + +/** + * The acquire provisioned a remote sandbox, the lease insert rejected it, the + * compensating teardown failed, and every retry of the durable pending-cleanup + * write also failed. A live sandbox now has no lease row and no cleanup record, + * so no sweep can find it. The acquire throws this error so the failure stays visible + * and never resolves to a clean rejection. The `cause` field holds the original + * lease insert rejection. The `cleanupWriteError` field holds the write failure. + */ +export class SandboxOrphanCleanupWriteError extends Error { + readonly provider: string; + readonly providerLeaseId: string | null; + readonly cleanupWriteError: unknown; + + constructor(input: { + provider: string; + providerLeaseId: string | null; + cause: unknown; + cleanupWriteError: unknown; + }) { + super( + `Sandbox provider "${input.provider}" leaked lease ` + + `"${input.providerLeaseId ?? "unknown"}": the acquire could not tear it down ` + + "and could not record a durable pending-cleanup row. The live sandbox has no " + + "durable cleanup state and needs a manual teardown.", + { cause: input.cause }, + ); + this.name = "SandboxOrphanCleanupWriteError"; + this.provider = input.provider; + this.providerLeaseId = input.providerLeaseId; + this.cleanupWriteError = input.cleanupWriteError; + } } export interface EnvironmentRuntimeLeaseRecord { @@ -461,13 +561,32 @@ export interface EnvironmentRuntimeLeaseRecord { const DEFAULT_PLUGIN_SANDBOX_WORKER_READY_TIMEOUT_MS = 5_000; const DEFAULT_PLUGIN_SANDBOX_WORKER_READY_POLL_MS = 100; +// The durable pending-cleanup write is the only automated way to find a leaked +// sandbox. A single insert can lose to a transient database fault (a dropped +// connection, a lock timeout, a serialization conflict). So the acquire retries +// the write a few times with a short backoff before it gives up. A later +// attempt can still land the durable row that a sweep finds. +const DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_ATTEMPTS = 3; +const DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_BACKOFF_MS = 100; + +// A prolonged database outage after a failed teardown can buffer many orphan +// records in-process. The bound stops that buffer from growing without limit. A +// full buffer keeps the error log as the last durable handle for a new orphan. +const DEFAULT_DEFERRED_ORPHAN_CLEANUP_BUFFER_LIMIT = 256; + function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -function getLeaseDriverKey(lease: Pick, environment: Pick): string { +function getLeaseDriverKey( + lease: Pick, + environment: Pick | null, +): string { const leaseDriver = typeof lease.metadata?.driver === "string" ? lease.metadata.driver : null; - return leaseDriver ?? environment.driver; + // An orphan `pending_cleanup` row whose environment a delete removed keeps its + // driver in the metadata, so the sweep still finds the sandbox driver. Fall + // back to "sandbox" because only sandbox leases record orphan cleanup. + return leaseDriver ?? environment?.driver ?? "sandbox"; } function toEnvironmentLeaseSnapshot(row: typeof environmentLeases.$inferSelect): EnvironmentLease { @@ -870,13 +989,331 @@ function createSandboxEnvironmentDriver( pluginWorkerManager?: PluginWorkerManager; pluginWorkerReadyTimeoutMs?: number; pluginWorkerReadyPollMs?: number; + pendingCleanupWriteAttempts?: number; + pendingCleanupWriteBackoffMs?: number; + deferredOrphanCleanupBufferLimit?: number; + orphanCleanupSpool?: SandboxOrphanCleanupSpool; + orphanCleanupSpoolDir?: string; } = {}, ): EnvironmentRuntimeDriver { const pluginWorkerManager = options.pluginWorkerManager; const pluginWorkerReadyTimeoutMs = options.pluginWorkerReadyTimeoutMs ?? DEFAULT_PLUGIN_SANDBOX_WORKER_READY_TIMEOUT_MS; const pluginWorkerReadyPollMs = options.pluginWorkerReadyPollMs ?? DEFAULT_PLUGIN_SANDBOX_WORKER_READY_POLL_MS; + // The retry count is at least one attempt, so a zero or negative override + // never skips the durable write. The backoff is at least zero milliseconds. + const pendingCleanupWriteAttempts = Math.max( + 1, + options.pendingCleanupWriteAttempts ?? DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_ATTEMPTS, + ); + const pendingCleanupWriteBackoffMs = Math.max( + 0, + options.pendingCleanupWriteBackoffMs ?? DEFAULT_SANDBOX_ORPHAN_CLEANUP_WRITE_BACKOFF_MS, + ); + // The buffer holds at least one orphan, so a zero or negative override never + // drops every buffered record. + const deferredOrphanCleanupBufferLimit = Math.max( + 1, + options.deferredOrphanCleanupBufferLimit ?? DEFAULT_DEFERRED_ORPHAN_CLEANUP_BUFFER_LIMIT, + ); const environmentsSvc = environmentService(db); + // A live sandbox whose teardown failed needs a durable `pending_cleanup` row, + // so a sweep can find and release it. When every synchronous write attempt + // fails, the database is down but the process still runs. So the driver keeps + // the orphan record in this in-process buffer, and a later cleanup sweep + // flushes it back to the database. The buffer closes the common window where + // the database recovers after the synchronous attempts but while the process + // still runs. The `pending_cleanup` failure reason matches the synchronous + // write, so a flushed row and a synchronous row are indistinguishable to the + // sweep. + const deferredOrphanCleanups: DeferredOrphanCleanupRecord[] = []; + const DEFERRED_ORPHAN_CLEANUP_FAILURE_REASON = "acquire_rejected_teardown_failed"; + + // The in-process buffer alone loses an orphan on a restart. So the driver also + // persists each buffered orphan to this durable local spool. The spool keeps + // the record on the local disk, next to the other durable server state, so a + // restart still finds it. The first flush after a restart loads the spool back + // into the buffer, so the sweep re-inserts the durable row. The spool holds no + // secret value: it persists the same sanitized metadata the database row + // holds, so a restart-recovered record still authorizes the teardown but never + // exposes a credential. Only a crash during the outage before the durable + // spool write lands loses the record; the error log stays the last handle for + // that narrow case. + const orphanCleanupSpool = + options.orphanCleanupSpool ?? createSandboxOrphanCleanupSpool(options.orphanCleanupSpoolDir); + + // Add one orphan record to the in-process buffer. Report `false` only when the + // buffer is full, so the caller keeps the error log as the last durable handle. + const enqueueDeferredOrphanCleanup = (record: DeferredOrphanCleanupRecord): boolean => { + // Dedup by the provider lease id, so a repeated failure for the same orphan + // never buffers it twice. Each acquire mints a unique provider lease id, so + // two distinct orphans never collide. Skip the dedup for a null lease id, + // because a null value carries no identity. + const alreadyBuffered = + record.providerLeaseId !== null && + deferredOrphanCleanups.some((entry) => entry.providerLeaseId === record.providerLeaseId); + if (alreadyBuffered) return true; + if (deferredOrphanCleanups.length >= deferredOrphanCleanupBufferLimit) return false; + deferredOrphanCleanups.push(record); + return true; + }; + + // Load the durable spool into the in-process buffer exactly once. The first + // flush after a restart runs this load, so the records a previous process + // could not land re-enter the flush path. A later flush sees the resolved + // promise and never reloads, so a record this process already buffered never + // doubles. The load runs before the flush splices the buffer, so a + // restart-recovered record flushes on the same tick. + let spoolLoadPromise: Promise | null = null; + const ensureSpoolLoaded = (): Promise => { + if (!spoolLoadPromise) { + spoolLoadPromise = (async () => { + const persisted = await orphanCleanupSpool.load(); + for (const record of persisted) { + enqueueDeferredOrphanCleanup(record); + } + })(); + } + return spoolLoadPromise; + }; + + const flushDeferredOrphanCleanups = async (): Promise<{ recovered: number; pending: number }> => { + // Load the durable spool into the buffer first, so a restart-recovered record + // flushes on this tick. The load runs once, then a resolved promise returns. + await ensureSpoolLoaded(); + if (deferredOrphanCleanups.length === 0) { + return { recovered: 0, pending: 0 }; + } + // Take the whole batch synchronously before the first await, so a second + // concurrent flush sees an empty buffer and never re-inserts the same record. + const batch = deferredOrphanCleanups.splice(0, deferredOrphanCleanups.length); + let recovered = 0; + for (const record of batch) { + try { + await environmentsSvc.insertPendingCleanupLease({ + companyId: record.companyId, + environmentId: record.environmentId, + executionWorkspaceId: record.executionWorkspaceId, + issueId: record.issueId, + heartbeatRunId: record.heartbeatRunId, + provider: record.provider, + providerLeaseId: record.providerLeaseId, + metadata: record.metadata, + failureReason: DEFERRED_ORPHAN_CLEANUP_FAILURE_REASON, + }); + // The durable row exists now, so the sweep finds and releases the orphan. + // Drop the spooled copy only after the row lands. A crash between the two + // leaves the spooled copy for a later flush, which re-inserts a duplicate + // the idempotent teardown handles. This order never loses the orphan. + await orphanCleanupSpool.remove(record); + recovered += 1; + } catch { + // The database is still down. Re-queue the record for a later flush, + // unless the buffer filled again. The identifiers carry no secret; the + // caught write exception never enters the log, because it can hold a + // credential in its message, code, cause, or stack. + const requeued = enqueueDeferredOrphanCleanup(record); + logger.warn( + { + errorKind: SANDBOX_ORPHAN_CLEANUP_WRITE_ERROR_KIND, + provider: record.provider, + providerLeaseId: record.providerLeaseId, + companyId: record.companyId, + environmentId: record.environmentId, + requeued, + }, + requeued + ? "deferred sandbox orphan cleanup flush failed; kept the orphan buffered for a later flush" + : "deferred sandbox orphan cleanup flush failed and the in-process buffer is full; live sandbox needs a manual teardown", + ); + } + } + return { recovered, pending: deferredOrphanCleanups.length }; + }; + + // Build the safe diagnostic fields for an orphan record. The provider + // identifiers are the only fields any diagnostic log emits, and they carry no + // secret. A caught write exception never enters a log: it can carry a + // credential in its name, code, message, cause, or stack. + const orphanDiagnosticFields = (record: DeferredOrphanCleanupRecord) => ({ + errorKind: SANDBOX_ORPHAN_CLEANUP_WRITE_ERROR_KIND, + provider: record.provider, + providerLeaseId: record.providerLeaseId, + companyId: record.companyId, + environmentId: record.environmentId, + executionWorkspaceId: record.executionWorkspaceId, + issueId: record.issueId, + heartbeatRunId: record.heartbeatRunId, + }); + + // Try to write the durable `pending_cleanup` row for an orphan sandbox, with a + // few retries. Report the new lease id on success, or a null lease id and the + // last write error on total failure. This function never buffers, never logs + // at error level, and never throws. The caller decides whether a total failure + // is fatal, because a later successful teardown removes the orphan and needs + // no durable row. + // + // The row carries the provider lease id and the same resolution metadata a + // normal lease holds, so a later cleanup sweep finds and releases the orphan. + // One atomic insert writes the row directly in the terminal `pending_cleanup` + // state, and the insert skips the company-binding assertion, so it records the + // orphan even for a foreign-bound environment. A transient database fault (a + // dropped connection, a lock timeout, a serialization conflict) can reject one + // insert but clear on the next, so the write retries a few times with a short + // backoff. + const tryWriteDurablePendingCleanup = async ( + record: DeferredOrphanCleanupRecord, + ): Promise<{ leaseId: string | null; lastError: unknown }> => { + const diagnosticFields = orphanDiagnosticFields(record); + let lastCleanupWriteError: unknown; + for (let attempt = 1; attempt <= pendingCleanupWriteAttempts; attempt += 1) { + try { + const lease = await environmentsSvc.insertPendingCleanupLease({ + companyId: record.companyId, + environmentId: record.environmentId, + executionWorkspaceId: record.executionWorkspaceId, + issueId: record.issueId, + heartbeatRunId: record.heartbeatRunId, + provider: record.provider, + providerLeaseId: record.providerLeaseId, + metadata: record.metadata, + failureReason: DEFERRED_ORPHAN_CLEANUP_FAILURE_REASON, + }); + // The durable row exists now, so a sweep can find and release the orphan. + return { leaseId: lease.id, lastError: undefined }; + } catch (cleanupWriteError) { + lastCleanupWriteError = cleanupWriteError; + if (attempt < pendingCleanupWriteAttempts) { + // The write failed, but attempts remain. Log the retry at warn level + // and back off, then try again. A later attempt can still land the + // durable row. + logger.warn( + { ...diagnosticFields, attempt, maxAttempts: pendingCleanupWriteAttempts }, + "sandbox orphan cleanup write failed; retrying the durable pending-cleanup write", + ); + await delay(pendingCleanupWriteBackoffMs * attempt); + } + } + } + return { leaseId: null, lastError: lastCleanupWriteError }; + }; + + // Release the durable `pending_cleanup` row after a successful inline teardown. + // The teardown removed the orphan sandbox, so the row is no longer needed. The + // release moves the row to the terminal `expired` state with a `success` + // cleanup status, the same state the sweep records for a successful teardown. + // + // The release is best-effort. If it fails, the row stays `pending_cleanup`, so + // a later sweep runs the idempotent teardown on the already-gone sandbox and + // releases the row itself. A failed release never loses or leaks the orphan. + const releaseCleanedUpOrphanRow = async ( + leaseId: string, + diagnosticFields: Record, + ): Promise => { + try { + await environmentsSvc.releaseLease(leaseId, "expired", { + cleanupStatus: "success", + failureReason: "acquire_rejected_teardown_succeeded", + }); + } catch { + // The caught release exception never enters a log: it can carry a + // credential in its name, code, message, cause, or stack. + logger.warn( + diagnosticFields, + "could not release the pending-cleanup row after a successful orphan teardown; a later sweep reconciles the released row", + ); + } + }; + + // Neither the durable write nor the inline teardown worked, so the orphan + // sandbox is live with no durable row. Persist the orphan to the durable spool + // and buffer it in-process, so a later cleanup-sweep flush lands the durable + // row. The spool survives a restart, so the flush recovers the orphan even + // after a crash. Then log the plain provider identifiers at error level, so an + // operator keeps a handle to tear the sandbox down by hand. Only a crash before + // the spool write lands loses the record, so the error log is the last handle + // for that narrow case. Throw `SandboxOrphanCleanupWriteError`, which the + // caller propagates. The error keeps the original insert rejection as its + // `cause`, so the real reason the acquire failed stays visible in the error + // chain. + const escalateUnwrittenOrphan = async ( + record: DeferredOrphanCleanupRecord, + cause: unknown, + cleanupWriteError: unknown, + ): Promise => { + const persisted = await orphanCleanupSpool.append(record); + const buffered = enqueueDeferredOrphanCleanup(record); + logger.error( + { + ...orphanDiagnosticFields(record), + persisted, + buffered, + deferredBufferSize: deferredOrphanCleanups.length, + }, + persisted + ? "sandbox orphan cleanup write failed; persisted the orphan to the durable spool for a later cleanup-sweep flush" + : buffered + ? "sandbox orphan cleanup write failed and the durable spool write failed; buffered the orphan in-process only for a later cleanup-sweep flush" + : "sandbox orphan cleanup write failed and neither the durable spool nor the in-process buffer kept the orphan; live sandbox needs a manual teardown", + ); + throw new SandboxOrphanCleanupWriteError({ + provider: record.provider, + providerLeaseId: record.providerLeaseId, + cause, + cleanupWriteError, + }); + }; + + // Clean up an orphan sandbox after the conditional lease insert rejected it. + // The acquire provisioned a remote sandbox, then the insert rejected (a + // foreign-company binding), so no lease row tracks the live sandbox. This + // handler records the durable `pending_cleanup` row FIRST, before the inline + // teardown, then tears the sandbox down. + // + // The write goes first on purpose. The conditional insert just returned a + // definitive rejection, so the database is reachable at this instant. An + // inline teardown can run long, and the database can drop during it. So a + // write after the teardown can fail and leave the orphan only in memory, which + // a restart loses. With the write first, the durable row lands while the + // database is proven up, so a crash after it still leaves a row that a sweep + // finds and releases. + // + // The teardown then runs. On success the orphan is gone, so the handler + // releases the durable row. On a failed teardown the row stays for the sweep. + // If the durable write itself failed and the teardown also failed, the handler + // buffers the orphan and throws `SandboxOrphanCleanupWriteError`. A successful + // teardown after a failed write leaves no orphan, so it needs no error. + const cleanUpRejectedOrphanSandbox = async (input: { + record: DeferredOrphanCleanupRecord; + cause: unknown; + canTeardown: boolean; + teardown: () => Promise; + }): Promise => { + const durable = await tryWriteDurablePendingCleanup(input.record); + let teardownFailed = !input.canTeardown; + if (!teardownFailed) { + try { + await input.teardown(); + } catch { + teardownFailed = true; + } + } + if (!teardownFailed) { + // The teardown removed the orphan, so drop the durable row if we wrote one. + if (durable.leaseId !== null) { + await releaseCleanedUpOrphanRow(durable.leaseId, orphanDiagnosticFields(input.record)); + } + return; + } + // The teardown failed, so the orphan sandbox is still live. + if (durable.leaseId !== null) { + // The durable row already tracks the orphan, so a sweep finds and releases + // it. The caller rethrows the original insert rejection. + return; + } + await escalateUnwrittenOrphan(input.record, input.cause, durable.lastError); + }; + // The run-time exec parent context, held per lease id. A plugin sandbox // provider can open a persistent session on the first command and delete it // on lease release. The session open runs inside `execute`, under the run @@ -949,7 +1386,7 @@ function createSandboxEnvironmentDriver( } async function resolvePluginSandboxRuntimeConfig(input: { - environment: Environment; + environment: Pick; lease: EnvironmentLease; provider: string; }): Promise> { @@ -1248,6 +1685,11 @@ function createSandboxEnvironmentDriver( // environment's default adapter image (a pi agent then runs in the // opencode image and the harness binary is missing at exec time). adapterType: input.adapterType ?? undefined, + // Forward a caller deadline so the provider configures a provider-side + // expiry at or before it and returns the real provider expiry. + ...(requestedExpiresAtParam(input.requestedExpiresAt) !== undefined + ? { requestedExpiresAt: requestedExpiresAtParam(input.requestedExpiresAt) } + : {}), }, resolvePluginSandboxRpcTimeoutMs(workerConfig), ); @@ -1276,28 +1718,83 @@ function createSandboxEnvironmentDriver( }) : null; - return await environmentsSvc.acquireLease({ - companyId: input.companyId, - environmentId: input.environment.id, - executionWorkspaceId: input.executionWorkspaceId, - issueId: input.issueId, - heartbeatRunId: input.heartbeatRunId, - leasePolicy: resolvedLeasePolicy, - provider: parsed.config.provider, - providerLeaseId: acquiredLease.providerLeaseId, - expiresAt: acquiredLease.expiresAt ? new Date(acquiredLease.expiresAt) : undefined, - metadata: { - ...(input.agentId ? { agentId: input.agentId } : {}), - driver: input.environment.driver, - executionWorkspaceMode: input.executionWorkspaceMode, - pluginId: pluginProvider.resolved.plugin.id, - pluginKey: pluginProvider.resolved.plugin.pluginKey, - sandboxProviderPlugin: true, - ...sandboxConfigForLeaseMetadata(storedConfig), - ...sanitizedProviderMetadata, - ...(reusableScope ? { reusableSandboxLease: reusableScope } : {}), - }, - }); + const pluginLeaseMetadata = { + ...(input.agentId ? { agentId: input.agentId } : {}), + driver: input.environment.driver, + executionWorkspaceMode: input.executionWorkspaceMode, + pluginId: pluginProvider.resolved.plugin.id, + pluginKey: pluginProvider.resolved.plugin.pluginKey, + sandboxProviderPlugin: true, + ...sandboxConfigForLeaseMetadata(storedConfig), + ...sanitizedProviderMetadata, + ...(reusableScope ? { reusableSandboxLease: reusableScope } : {}), + }; + try { + return await environmentsSvc.acquireLease({ + companyId: input.companyId, + environmentId: input.environment.id, + executionWorkspaceId: input.executionWorkspaceId, + issueId: input.issueId, + heartbeatRunId: input.heartbeatRunId, + assertCompanyBinding: input.assertCompanyBinding, + leasePolicy: resolvedLeasePolicy, + provider: parsed.config.provider, + providerLeaseId: acquiredLease.providerLeaseId, + expiresAt: providerAttestedLeaseExpiry( + input.requestedExpiresAt, + acquiredLease.expiresAt ? new Date(acquiredLease.expiresAt) : undefined, + ), + metadata: pluginLeaseMetadata, + }); + } catch (error) { + // The conditional lease insert rejected, so no lease row exists. A + // managed reconciliation can bind the environment to another company + // between the route guard and this insert, so the insert fails closed + // with `environment_company_mismatch`. This call already provisioned the + // remote plugin sandbox above, so tear it down now. Without this step + // the rejected insert leaks a live sandbox that no lease row tracks. The + // Claude login runs on a plugin-backed sandbox, so this path is the one + // the login uses. Do not tear down a reused sandbox that an earlier + // lease still owns. + if (!reusableLease || acquiredLease.providerLeaseId !== reusableLease.providerLeaseId) { + // Record the durable pending-cleanup row before the teardown, then + // tear the sandbox down. On a successful teardown the handler releases + // the row. If the durable write and the teardown both fail, the + // handler throws a `SandboxOrphanCleanupWriteError` that carries the + // original rejection as its cause. + await cleanUpRejectedOrphanSandbox({ + record: { + companyId: input.companyId, + environmentId: input.environment.id, + executionWorkspaceId: input.executionWorkspaceId ?? null, + issueId: input.issueId ?? null, + heartbeatRunId: input.heartbeatRunId ?? null, + provider: parsed.config.provider, + providerLeaseId: acquiredLease.providerLeaseId, + metadata: pluginLeaseMetadata, + }, + cause: error, + canTeardown: pluginWorkerManager.isRunning(pluginProvider.resolved.plugin.id), + teardown: async () => { + await pluginWorkerManager.call( + pluginProvider.resolved.plugin.id, + "environmentDestroyLease", + { + driverKey: parsed.config.provider, + companyId: input.companyId, + environmentId: input.environment.id, + issueId: input.issueId, + config: workerConfig, + providerLeaseId: acquiredLease.providerLeaseId, + leaseMetadata: acquiredLease.metadata ?? undefined, + }, + resolvePluginSandboxRpcTimeoutMs(workerConfig), + ); + }, + }); + } + throw error; + } } // Built-in sandbox provider path. Same guard as the plugin-backed path: @@ -1390,6 +1887,9 @@ function createSandboxEnvironmentDriver( agentId: input.agentId, executionWorkspaceId: input.executionWorkspaceId, reusableProviderLeaseId, + // Forward a caller deadline so the provider configures a provider-side + // expiry at or before it and returns the real provider expiry. + requestedExpiresAt: requestedExpiresAtParam(input.requestedExpiresAt), }); } catch (error) { if (reusableLease) { @@ -1428,23 +1928,67 @@ function createSandboxEnvironmentDriver( }) : null; - return await environmentsSvc.acquireLease({ - companyId: input.companyId, - environmentId: input.environment.id, - executionWorkspaceId: input.executionWorkspaceId, - issueId: input.issueId, - heartbeatRunId: input.heartbeatRunId, - leasePolicy: resolvedLeasePolicy, - provider: parsed.config.provider, - providerLeaseId: providerLease.providerLeaseId, - metadata: { - ...(input.agentId ? { agentId: input.agentId } : {}), - driver: input.environment.driver, - executionWorkspaceMode: input.executionWorkspaceMode, - ...providerLease.metadata, - ...(reusableScope ? { reusableSandboxLease: reusableScope } : {}), - }, - }); + const builtinLeaseMetadata = { + ...(input.agentId ? { agentId: input.agentId } : {}), + driver: input.environment.driver, + executionWorkspaceMode: input.executionWorkspaceMode, + ...providerLease.metadata, + ...(reusableScope ? { reusableSandboxLease: reusableScope } : {}), + }; + try { + return await environmentsSvc.acquireLease({ + companyId: input.companyId, + environmentId: input.environment.id, + executionWorkspaceId: input.executionWorkspaceId, + issueId: input.issueId, + heartbeatRunId: input.heartbeatRunId, + assertCompanyBinding: input.assertCompanyBinding, + leasePolicy: resolvedLeasePolicy, + provider: parsed.config.provider, + providerLeaseId: providerLease.providerLeaseId, + expiresAt: providerAttestedLeaseExpiry( + input.requestedExpiresAt, + providerLease.expiresAt ? new Date(providerLease.expiresAt) : undefined, + ), + metadata: builtinLeaseMetadata, + }); + } catch (error) { + // The conditional lease insert rejected, so no lease row exists. A managed + // reconciliation can bind the environment to another company between the + // route guard and this insert, so the insert fails closed with + // `environment_company_mismatch`. This call already provisioned the remote + // sandbox above, so release it now. Without this teardown the rejected + // insert leaks a live sandbox that no lease row tracks. Do not tear down a + // reused sandbox that an earlier lease still owns. + if (!reusableLease || providerLease.providerLeaseId !== reusableLease.providerLeaseId) { + // Record the durable pending-cleanup row before the teardown, then + // release the remote sandbox. On a successful teardown the handler + // releases the row. If the durable write and the teardown both fail, + // the handler throws a `SandboxOrphanCleanupWriteError` that carries the + // original rejection as its cause. + await cleanUpRejectedOrphanSandbox({ + record: { + companyId: input.companyId, + environmentId: input.environment.id, + executionWorkspaceId: input.executionWorkspaceId ?? null, + issueId: input.issueId ?? null, + heartbeatRunId: input.heartbeatRunId ?? null, + provider: parsed.config.provider, + providerLeaseId: providerLease.providerLeaseId, + metadata: builtinLeaseMetadata, + }, + cause: error, + canTeardown: true, + teardown: async () => { + await destroySandboxProviderLease({ + config: parsed.config, + providerLeaseId: providerLease.providerLeaseId, + }); + }, + }); + } + throw error; + } }, async releaseRunLease(input) { @@ -1501,6 +2045,140 @@ function createSandboxEnvironmentDriver( }); }, + async retryPendingSandboxTeardown(input) { + // Resolve the teardown from the immutable orphan lease row, not from the + // current environment. The row keeps the provider, the provider lease id, + // and the sandbox config in its metadata. A provider change re-points the + // environment, and an environment delete removes it, but neither must + // strand this teardown. So the retry reads the recorded provider and the + // recorded config, and never the current environment provider. + const recordedProvider = + input.lease.provider ?? + (typeof input.lease.metadata?.provider === "string" ? input.lease.metadata.provider : null); + if (!recordedProvider) { + throw new Error(`Pending-cleanup lease "${input.lease.id}" has no recorded provider for teardown.`); + } + + // Build the config from the lease metadata under the orphan's company + // scope, so the retry resolves the same connection secrets the failed + // acquire used. The recorded config is the sole source of truth here: a + // delete removed the environment, and a provider change re-points it, so + // the retry never reads the current environment config. + const metadataConfig = sandboxConfigFromLeaseMetadataLoose(input.lease); + if (!metadataConfig || metadataConfig.provider !== recordedProvider) { + throw new Error( + `Pending-cleanup lease "${input.lease.id}" has no recorded config for provider "${recordedProvider}".`, + ); + } + + // Plugin-backed provider path. The Claude login runs on a plugin-backed + // sandbox, so this path tears down the login orphan. + if (!isBuiltinSandboxProvider(recordedProvider)) { + if (!pluginWorkerManager) { + throw new Error( + `Sandbox provider "${recordedProvider}" needs a plugin worker manager for cleanup, but none is available.`, + ); + } + const pluginProvider = await resolveSandboxProviderPlugin({ provider: recordedProvider }); + if (pluginProvider.state !== "running") { + throw new Error( + `Sandbox provider plugin for "${recordedProvider}" is ${pluginProvider.state}, so the cleanup teardown cannot run yet.`, + ); + } + // Resolve the recorded secret refs through the durable orphan record, + // not the environment binding. A delete removed the binding, or a + // provider change replaced it, so environment-bound resolution would + // reject the recorded API-key ref and strand the teardown forever. + const config = await resolveSandboxCleanupConfigSecrets( + db, + input.lease.companyId, + metadataConfig, + { issueId: input.lease.issueId, heartbeatRunId: input.lease.heartbeatRunId }, + ); + const workerConfig = stripSandboxProviderEnvelope(config as SandboxEnvironmentConfig); + await pluginWorkerManager.call( + pluginProvider.resolved.plugin.id, + "environmentDestroyLease", + { + driverKey: recordedProvider, + companyId: input.lease.companyId, + // The provider teardown keys on the provider lease id. The + // environment id is only context, and it is empty when a delete + // removed the environment before this orphan was recorded. + environmentId: input.lease.environmentId ?? input.environment?.id ?? "", + issueId: input.lease.issueId, + config: workerConfig, + providerLeaseId: input.lease.providerLeaseId, + leaseMetadata: input.lease.metadata ?? undefined, + }, + resolvePluginSandboxRpcTimeoutMs(workerConfig), + ); + return; + } + + // Built-in provider path. Resolve the recorded config secrets through the + // durable orphan record, not the environment binding, for the same reason + // as the plugin path above. The teardown targets the recorded provider, + // never the current environment provider. + const cleanupConfig = await resolveSandboxCleanupConfigSecrets( + db, + input.lease.companyId, + metadataConfig, + { issueId: input.lease.issueId, heartbeatRunId: input.lease.heartbeatRunId }, + ); + await destroySandboxProviderLease({ + config: cleanupConfig, + providerLeaseId: input.lease.providerLeaseId, + }); + }, + + async isPendingCleanupWorkerReady(input) { + // Resolve the recorded provider the same way `retryPendingSandboxTeardown` + // does, so the probe reads the same target the teardown uses. + const recordedProvider = + input.lease.provider ?? + (typeof input.lease.metadata?.provider === "string" ? input.lease.metadata.provider : null); + // A missing provider is a permanent misconfiguration. Report ready, so the + // teardown runs, throws, and counts toward the cap. + if (!recordedProvider) return true; + // A built-in provider has no plugin worker, so it is always ready. + if (isBuiltinSandboxProvider(recordedProvider)) return true; + // No worker manager is a permanent condition here. Report ready, so the + // teardown runs, throws its own "no worker manager" error, and counts + // toward the cap. + if (!pluginWorkerManager) return true; + // Resolve the installed plugin without a wait. A plugin reload or a plugin + // reinstall can remove the plugin row for a short window, so a missing + // plugin is a transient condition, not a permanent one. Report not ready, + // so the sweep skips the lease without a claim, and a later sweep retries + // after the plugin returns. A teardown while the plugin is missing only + // throws and burns a finite attempt, so a long reload could exhaust the + // retries and strand the sandbox. A permanent uninstall also cannot tear + // the sandbox down, because there is no worker to call, so the preserved + // pending_cleanup row still holds the durable cleanup state for later. + const installed = await resolvePluginSandboxProviderDriverByKey({ + db, + driverKey: recordedProvider, + workerManager: pluginWorkerManager, + requireRunning: false, + }); + if (!installed) return false; + // The plugin is installed but not ready yet. A plugin reload or a plugin + // reinstall moves the plugin through this state, so it is a transient + // window, not a permanent condition. Report not ready, so the sweep skips + // the lease without a claim, and a later sweep retries after the plugin + // becomes ready. A teardown here only throws "is not_ready" and burns a + // finite attempt, so a long reload could exhaust the retries and strand the + // sandbox. + if (installed.plugin.status !== "ready") return false; + // The plugin is installed and ready, so gate on the live worker. A running + // worker is ready. A down worker is the transient restart window, so report + // not ready and let a later sweep retry after the worker recovers. + return pluginWorkerManager.isRunning(installed.plugin.id); + }, + + flushDeferredOrphanCleanups, + async realizeWorkspace(input) { // Resolve the realized cwd and any provider metadata first, then build ONE // workspace-realization record and wrap it the SAME way for every driver. A @@ -1922,6 +2600,44 @@ function parseExpiresAt(value: string | null | undefined): Date | null { return Number.isNaN(parsed.getTime()) ? null : parsed; } +/** + * Resolves the persisted lease expiry for a caller-requested deadline. It records + * ONLY a provider-attested expiry as evidence of provider enforcement. It never + * synthesizes the requested deadline onto the lease row: a database-only expiry + * does not stop a remote sandbox after a server crash, so it must not stand in for + * a provider-side bound. It returns the real provider expiry when the caller sets + * a deadline (the caller then verifies the expiry bounds the deadline and fails + * closed when it does not). It returns null when the caller sets a deadline and the + * provider grants no expiry. It returns the provider expiry unchanged when the + * caller requests no deadline, so all other callers keep the current behavior. It + * ignores an invalid requested deadline. + */ +function providerAttestedLeaseExpiry( + requestedExpiresAt: Date | null | undefined, + providerExpiresAt: Date | null | undefined, +): Date | null | undefined { + const requested = + requestedExpiresAt instanceof Date && !Number.isNaN(requestedExpiresAt.getTime()) + ? requestedExpiresAt + : null; + if (!requested) return providerExpiresAt; + return providerExpiresAt ?? null; +} + +/** + * Converts a caller-requested deadline to the ISO 8601 string a provider acquire + * RPC carries. It returns undefined for an absent or invalid deadline, so a + * generic caller without a deadline sends no requested expiry and keeps the + * current provider behavior. + */ +function requestedExpiresAtParam( + requestedExpiresAt: Date | null | undefined, +): string | undefined { + return requestedExpiresAt instanceof Date && !Number.isNaN(requestedExpiresAt.getTime()) + ? requestedExpiresAt.toISOString() + : undefined; +} + function pluginDriverProviderKey(config: PluginEnvironmentConfig): string { return `${config.pluginKey}:${config.driverKey}`; } @@ -2081,6 +2797,11 @@ function createPluginEnvironmentDriver( executionWorkspaceId: input.executionWorkspaceId ?? undefined, adapterType: input.adapterType ?? undefined, executionWorkspaceSettings: input.executionWorkspaceSettings, + // Forward a caller deadline so the provider configures a provider-side + // expiry at or before it and returns the real provider expiry. + ...(requestedExpiresAtParam(input.requestedExpiresAt) !== undefined + ? { requestedExpiresAt: requestedExpiresAtParam(input.requestedExpiresAt) } + : {}), } as PluginEnvironmentAcquireLeaseParams); return await environmentsSvc.acquireLease({ @@ -2092,7 +2813,7 @@ function createPluginEnvironmentDriver( leasePolicy: "ephemeral", provider: `plugin:${parsed.config.pluginKey}:${parsed.config.driverKey}`, providerLeaseId: providerLease.providerLeaseId, - expiresAt: parseExpiresAt(providerLease.expiresAt), + expiresAt: providerAttestedLeaseExpiry(input.requestedExpiresAt, parseExpiresAt(providerLease.expiresAt)), metadata: { ...(input.agentId ? { agentId: input.agentId } : {}), providerMetadata: providerLease.metadata ?? {}, @@ -2243,6 +2964,11 @@ export function environmentRuntimeService( pluginWorkerManager?: PluginWorkerManager; pluginWorkerReadyTimeoutMs?: number; pluginWorkerReadyPollMs?: number; + pendingCleanupWriteAttempts?: number; + pendingCleanupWriteBackoffMs?: number; + deferredOrphanCleanupBufferLimit?: number; + orphanCleanupSpool?: SandboxOrphanCleanupSpool; + orphanCleanupSpoolDir?: string; } = {}, ) { const environmentsSvc = environmentService(db); @@ -2255,6 +2981,11 @@ export function environmentRuntimeService( pluginWorkerManager: options.pluginWorkerManager, pluginWorkerReadyTimeoutMs: options.pluginWorkerReadyTimeoutMs, pluginWorkerReadyPollMs: options.pluginWorkerReadyPollMs, + pendingCleanupWriteAttempts: options.pendingCleanupWriteAttempts, + pendingCleanupWriteBackoffMs: options.pendingCleanupWriteBackoffMs, + deferredOrphanCleanupBufferLimit: options.deferredOrphanCleanupBufferLimit, + orphanCleanupSpool: options.orphanCleanupSpool, + orphanCleanupSpoolDir: options.orphanCleanupSpoolDir, }), ...(options.pluginWorkerManager ? [createPluginEnvironmentDriver(db, options.pluginWorkerManager)] @@ -2309,6 +3040,18 @@ export function environmentRuntimeService( * lease uses the operator-prepared custom image. */ applyCustomImageTemplate?: boolean; + /** + * The latest time the acquired lease may stay active. The driver bounds + * the persisted lease expiry to this time. Null or undefined keeps the + * provider expiry only. + */ + requestedExpiresAt?: Date | null; + /** + * Re-check the environment company binding inside the lease insert + * transaction. The login acquire paths set this to reject a foreign-company + * environment with the 403 `environment_company_mismatch`. + */ + assertCompanyBinding?: boolean; }): Promise { if (input.environment.status !== "active") { throw new Error(`Environment "${input.environment.name}" is not active.`); @@ -2329,6 +3072,8 @@ export function environmentRuntimeService( executionWorkspaceSettings: input.executionWorkspaceSettings ?? null, adapterType: input.adapterType ?? null, applyCustomImageTemplate: input.applyCustomImageTemplate ?? false, + requestedExpiresAt: input.requestedExpiresAt ?? null, + assertCompanyBinding: input.assertCompanyBinding, }); return { @@ -2363,7 +3108,9 @@ export function environmentRuntimeService( const released: EnvironmentRuntimeLeaseRecord[] = []; for (const leaseRow of leaseRows) { try { - const environment = await environmentsSvc.getById(leaseRow.environmentId); + const environment = leaseRow.environmentId + ? await environmentsSvc.getById(leaseRow.environmentId) + : null; if (!environment) continue; const leaseSnapshot = toEnvironmentLeaseSnapshot(leaseRow); @@ -2394,6 +3141,58 @@ export function environmentRuntimeService( return released; }, + // Tear an orphan ephemeral sandbox down from its recorded provider config. + // A failed acquire records the orphan as a `pending_cleanup` lease. The row + // keeps the provider, the provider lease id, and the sandbox config, so this + // teardown runs without the environment row and accepts a null environment. + // The teardown resolves the recorded secret refs through the durable orphan + // record, so a deleted or foreign-bound environment never strands it. The + // caller owns the lease release; this dispatcher only runs the provider + // teardown and throws when the driver teardown throws. + async retryPendingSandboxTeardown(input: { + environment: Environment | null; + lease: EnvironmentLease; + }): Promise { + const driver = requireDriverKey(getLeaseDriverKey(input.lease, input.environment)); + if (!driver.retryPendingSandboxTeardown) { + throw new Error( + `Environment driver "${driver.driver}" does not support orphan sandbox teardown.`, + ); + } + await driver.retryPendingSandboxTeardown(input); + }, + + // Report whether the provider worker can run an orphan teardown now. The + // cleanup sweep calls this before it claims a finite retry attempt, so a + // briefly-down plugin worker never burns an attempt. This dispatcher never + // throws: an unregistered driver, or a driver with no probe, reports ready, + // so the teardown still runs and its own failure counts toward the cap. + async isPendingCleanupWorkerReady(input: { + environment: Environment | null; + lease: EnvironmentLease; + }): Promise { + const driver = getDriver(getLeaseDriverKey(input.lease, input.environment)); + if (!driver?.isPendingCleanupWorkerReady) return true; + return driver.isPendingCleanupWorkerReady(input); + }, + + // Flush the in-process orphan-cleanup buffers of every driver that keeps one. + // The cleanup sweep calls this each tick, so a buffered orphan lands a durable + // `pending_cleanup` row once the database recovers, and the same sweep tears + // it down. The dispatcher sums the recovered and pending counts across all + // drivers. A driver with no buffer contributes nothing. + async flushDeferredOrphanCleanups(): Promise<{ recovered: number; pending: number }> { + let recovered = 0; + let pending = 0; + for (const driver of drivers.values()) { + if (!driver.flushDeferredOrphanCleanups) continue; + const result = await driver.flushDeferredOrphanCleanups(); + recovered += result.recovered; + pending += result.pending; + } + return { recovered, pending }; + }, + async destroyReusableSandboxLeases(input: { companyId: string; issueId?: string | null; @@ -2420,7 +3219,9 @@ export function environmentRuntimeService( const destroyed: EnvironmentRuntimeLeaseRecord[] = []; for (const leaseRow of leaseRows) { - const environment = await environmentsSvc.getById(leaseRow.environmentId); + const environment = leaseRow.environmentId + ? await environmentsSvc.getById(leaseRow.environmentId) + : null; if (!environment) continue; const leaseSnapshot = toEnvironmentLeaseSnapshot(leaseRow); const driver = getDriver(getLeaseDriverKey(leaseSnapshot, environment)); diff --git a/server/src/services/environments.ts b/server/src/services/environments.ts index 736462b06b..11e7a825a7 100644 --- a/server/src/services/environments.ts +++ b/server/src/services/environments.ts @@ -30,7 +30,7 @@ import { type EnvironmentLeaseStatus, type UpdateEnvironment, } from "@paperclipai/shared"; -import { conflict } from "../errors.js"; +import { conflict, forbidden } from "../errors.js"; import { logActivity } from "./activity-log.js"; import { isCloudManagedInstance } from "./cloud-instance.js"; import { @@ -822,6 +822,26 @@ export function environmentService(db: Db) { return row ? toEnvironment(row) : null; }, + /** + * List the companies that own an environment through a built-in managed + * resource binding. A managed sandbox row binds to each company that the + * instance provisions it for. An operator-created environment has no + * binding, so this returns an empty list. The caller treats an empty list + * as an instance-global environment with no company owner. + */ + listBoundCompanyIds: async (environmentId: string): Promise => { + const rows = await db + .select({ companyId: builtInManagedResources.companyId }) + .from(builtInManagedResources) + .where( + and( + eq(builtInManagedResources.resourceKind, MANAGED_ENVIRONMENT_RESOURCE_KIND), + eq(builtInManagedResources.resourceId, environmentId), + ), + ); + return Array.from(new Set(rows.map((row) => row.companyId))); + }, + getLeaseById: async (id: string): Promise => { const row = await db .select() @@ -1089,6 +1109,29 @@ export function environmentService(db: Db) { select 1 from ${instanceSettings} where ${instanceSettings.defaultEnvironmentId} = ${environments.id} )`, + // A `pending_cleanup` lease is the durable teardown reference for an + // orphan sandbox. The environment foreign key uses + // `on delete set null`, so a delete keeps the lease row but drops its + // environment reference. This predicate refuses the delete while such + // a lease exists, so the operator resolves the cleanup first and the + // lease keeps its environment link. It runs in the same statement as + // the delete, so it also closes the check-to-delete race. + sql`not exists ( + select 1 from ${environmentLeases} + where ${environmentLeases.environmentId} = ${environments.id} + and ${environmentLeases.status} = 'pending_cleanup' + )`, + // A reusable lease keeps a live provider sandbox after a run + // releases it. Deleting the environment would set its reference to + // null, and both the normal release path and scoped reusable cleanup + // require that environment context. Refuse the delete atomically + // until the owning issue/workspace destroys the reusable sandbox. + sql`not exists ( + select 1 from ${environmentLeases} + where ${environmentLeases.environmentId} = ${environments.id} + and ${environmentLeases.leasePolicy} = 'reuse_by_environment' + and ${environmentLeases.status} in ('active', 'released', 'retained') + )`, ), ) .returning() @@ -1096,6 +1139,26 @@ export function environmentService(db: Db) { return row ? toEnvironment(row) : null; }, + /** + * Return true when the environment has one or more leases in the terminal + * `pending_cleanup` state. Each such lease is the only durable provider + * reference for an orphan sandbox that a teardown retry must destroy. The + * delete guard and the provider-change guard call this to protect that + * reference. + */ + hasUnresolvedPendingCleanupLeases: async (environmentId: string): Promise => { + const rows = await db + .select({ count: sql`count(*)::int` }) + .from(environmentLeases) + .where( + and( + eq(environmentLeases.environmentId, environmentId), + eq(environmentLeases.status, "pending_cleanup"), + ), + ); + return countFromRows(rows) > 0; + }, + getDeleteBlastRadius: async (id: string): Promise => { const environment = await db .select({ @@ -1115,6 +1178,8 @@ export function environmentService(db: Db) { projectRows, secretBindingRows, activeLeaseRows, + pendingCleanupLeaseRows, + reusableSandboxLeaseRows, activeSetupRows, ] = await Promise.all([ db @@ -1155,6 +1220,25 @@ export function environmentService(db: Db) { eq(environmentLeases.status, "active"), ), ), + db + .select({ count: sql`count(*)::int` }) + .from(environmentLeases) + .where( + and( + eq(environmentLeases.environmentId, id), + eq(environmentLeases.status, "pending_cleanup"), + ), + ), + db + .select({ count: sql`count(*)::int` }) + .from(environmentLeases) + .where( + and( + eq(environmentLeases.environmentId, id), + eq(environmentLeases.leasePolicy, "reuse_by_environment"), + inArray(environmentLeases.status, ["active", "released", "retained"]), + ), + ), db .select({ count: sql`count(*)::int` }) .from(environmentCustomImageSetupSessions) @@ -1168,9 +1252,18 @@ export function environmentService(db: Db) { const isManagedLocal = environment.driver === "local"; const isInstanceDefault = countFromRows(instanceDefaultRows) > 0; + const pendingCleanupLeaseCount = countFromRows(pendingCleanupLeaseRows); + const reusableSandboxLeaseCount = countFromRows(reusableSandboxLeaseRows); const deleteBlockedReasons: EnvironmentDeleteBlockedReason[] = []; if (isManagedLocal) deleteBlockedReasons.push("managed_local"); if (isInstanceDefault) deleteBlockedReasons.push("instance_default"); + // A `pending_cleanup` lease is the durable teardown reference for an orphan + // sandbox. The environment foreign key uses `on delete set null`, so a + // delete keeps the lease but drops its environment reference. Block the + // delete until the sweep resolves the lease, so the operator resolves the + // cleanup first and the lease keeps its environment link. + if (pendingCleanupLeaseCount > 0) deleteBlockedReasons.push("pending_sandbox_cleanup"); + if (reusableSandboxLeaseCount > 0) deleteBlockedReasons.push("reusable_sandbox_lease"); const activeLeaseCount = countFromRows(activeLeaseRows); const activeCustomImageSetupSessionCount = countFromRows(activeSetupRows); @@ -1178,6 +1271,8 @@ export function environmentService(db: Db) { environmentId: id, canDelete: deleteBlockedReasons.length === 0, deleteBlockedReasons, + pendingCleanupLeaseCount, + reusableSandboxLeaseCount, staticReferences: { isManagedLocal, isInstanceDefault, @@ -1222,32 +1317,80 @@ export function environmentService(db: Db) { providerLeaseId?: string | null; expiresAt?: Date | null; metadata?: Record | null; + /** + * Re-check the environment company binding inside the lease insert + * transaction. The login routes set this to close the check-to-lease + * race: managed reconciliation can bind a sandbox to another company + * between the route guard and this acquire. When the environment is + * bound to a company other than `companyId`, the insert throws the 403 + * `environment_company_mismatch` and no lease row is created (fail + * closed). An unbound (instance-global) environment stays open to every + * member. All other callers keep the plain, non-transactional insert. + */ + assertCompanyBinding?: boolean; }): Promise => { const now = new Date(); - const row = await db - .insert(environmentLeases) - .values({ - companyId: input.companyId, - environmentId: input.environmentId, - executionWorkspaceId: input.executionWorkspaceId ?? null, - issueId: input.issueId ?? null, - heartbeatRunId: input.heartbeatRunId ?? null, - status: "active", - leasePolicy: input.leasePolicy ?? "ephemeral", - provider: input.provider ?? null, - providerLeaseId: input.providerLeaseId ?? null, - acquiredAt: now, - lastUsedAt: now, - expiresAt: input.expiresAt ?? null, - releasedAt: null, - failureReason: null, - cleanupStatus: null, - metadata: input.metadata ?? null, - createdAt: now, - updatedAt: now, - }) - .returning() - .then((rows) => rows[0] ?? null); + const values = { + companyId: input.companyId, + environmentId: input.environmentId, + executionWorkspaceId: input.executionWorkspaceId ?? null, + issueId: input.issueId ?? null, + heartbeatRunId: input.heartbeatRunId ?? null, + status: "active" as const, + leasePolicy: input.leasePolicy ?? "ephemeral", + provider: input.provider ?? null, + providerLeaseId: input.providerLeaseId ?? null, + acquiredAt: now, + lastUsedAt: now, + expiresAt: input.expiresAt ?? null, + releasedAt: null, + failureReason: null, + cleanupStatus: null, + metadata: input.metadata ?? null, + createdAt: now, + updatedAt: now, + }; + const row = input.assertCompanyBinding + ? await db.transaction(async (tx) => { + // Lock the environment row first. Managed reconciliation locks the + // same sandbox environment rows with `for update` before it writes a + // company binding, so this lock serializes the two transactions on + // this row and closes the time-of-check to time-of-use window. + await tx + .select({ id: environments.id }) + .from(environments) + .where(eq(environments.id, input.environmentId)) + .for("update"); + // Re-read the company binding inside the locked transaction. A + // binding a reconciliation committed after the route guard now + // appears here. Reject a foreign-company environment before the + // insert, so the login holds no lease. + const boundRows = await tx + .select({ companyId: builtInManagedResources.companyId }) + .from(builtInManagedResources) + .where( + and( + eq(builtInManagedResources.resourceKind, MANAGED_ENVIRONMENT_RESOURCE_KIND), + eq(builtInManagedResources.resourceId, input.environmentId), + ), + ); + const boundCompanyIds = Array.from(new Set(boundRows.map((boundRow) => boundRow.companyId))); + if (boundCompanyIds.length > 0 && !boundCompanyIds.includes(input.companyId)) { + throw forbidden("The selected environment belongs to another company.", { + code: "environment_company_mismatch", + }); + } + return tx + .insert(environmentLeases) + .values(values) + .returning() + .then((rows) => rows[0] ?? null); + }) + : await db + .insert(environmentLeases) + .values(values) + .returning() + .then((rows) => rows[0] ?? null); if (!row) { throw new Error("Failed to acquire environment lease"); } @@ -1279,6 +1422,82 @@ export function environmentService(db: Db) { return row ? toEnvironmentLease(row) : null; }, + /** + * Record a lease-less orphan sandbox directly in the terminal + * `pending_cleanup` state with one atomic insert. The sweep reads a row with + * status `pending_cleanup` and cleanup status `failed`, so this insert makes + * the orphan visible to recovery immediately. It never passes through the + * `active` state, so a process or database crash cannot strand the row in an + * intermediate state that no sweep finds. The insert skips the company + * binding assertion, so it records the orphan even for a foreign-bound + * environment. + * + * The insert runs in a transaction that first locks the environment row with + * `for update`. The delete path (`removeIfDeletable`) refuses a delete while a + * `pending_cleanup` lease exists, so this lock serializes the two writes: + * + * - The environment still exists: the insert records the orphan with the + * environment reference. A concurrent delete blocks on the lock, then its + * `not exists (pending_cleanup)` predicate fails, so the delete refuses. + * - The environment is already gone (a delete won the race before this + * insert): the insert records the orphan with a null environment + * reference. The row carries the immutable provider metadata the sweep + * needs, so the teardown still runs. This closes the window the finding + * describes, where the foreign-key insert fails after a delete. + */ + insertPendingCleanupLease: async (input: { + companyId: string; + environmentId: string; + executionWorkspaceId?: string | null; + issueId?: string | null; + heartbeatRunId?: string | null; + provider?: string | null; + providerLeaseId?: string | null; + metadata?: Record | null; + failureReason: string; + }): Promise => { + const now = new Date(); + const row = await db.transaction(async (tx) => { + // Lock the environment row so a concurrent delete cannot commit between + // this read and the insert. An absent row means a delete already removed + // the environment, so record the orphan with a null reference. + const environmentRows = await tx + .select({ id: environments.id }) + .from(environments) + .where(eq(environments.id, input.environmentId)) + .for("update"); + const environmentIdForRow = environmentRows[0]?.id ?? null; + return tx + .insert(environmentLeases) + .values({ + companyId: input.companyId, + environmentId: environmentIdForRow, + executionWorkspaceId: input.executionWorkspaceId ?? null, + issueId: input.issueId ?? null, + heartbeatRunId: input.heartbeatRunId ?? null, + status: "pending_cleanup" as const, + leasePolicy: "ephemeral", + provider: input.provider ?? null, + providerLeaseId: input.providerLeaseId ?? null, + acquiredAt: now, + lastUsedAt: now, + expiresAt: null, + releasedAt: now, + failureReason: input.failureReason, + cleanupStatus: "failed", + metadata: input.metadata ?? null, + createdAt: now, + updatedAt: now, + }) + .returning() + .then((rows) => rows[0] ?? null); + }); + if (!row) { + throw new Error("Failed to record pending sandbox cleanup lease"); + } + return toEnvironmentLease(row); + }, + updateLeaseMetadata: async ( id: string, metadata: Record | null, diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 358fe7357f..5b3ceae2a7 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -13261,6 +13261,28 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) return claimed.length > 0; } + // Defer a pending_cleanup lease whose provider plugin is not ready this tick. + // The sweep reads one page of the oldest rows, ordered by `updatedAt`. A lease + // that the sweep only skips keeps its old `updatedAt`, so it stays the oldest + // and refills the page on every tick. That starves a newer lease whose + // provider is ready. The defer bumps `updatedAt` to now, so the unavailable + // lease moves to the back of the queue and a ready lease takes its page slot. + // The defer never writes the attempt count, so a long provider outage never + // consumes a finite retry. The status guard keeps the write on a lease that is + // still pending_cleanup. + async function deferPendingCleanupLease(leaseId: string): Promise { + const now = new Date(); + await db + .update(environmentLeases) + .set({ updatedAt: now }) + .where( + and( + eq(environmentLeases.id, leaseId), + eq(environmentLeases.status, "pending_cleanup"), + ), + ); + } + // Retry the leases stranded in "pending_cleanup". A failed destroy leaves a // lease in that state forever without this sweep. The reaper tick runs the // sweep. The backoff equals the reaper staleness threshold, so a lease waits @@ -13276,6 +13298,27 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const now = new Date(); const cutoff = new Date(now.getTime() - backoffMs); + // Flush the in-process orphan-cleanup buffer first. A failed acquire buffers + // an orphan there when every synchronous pending-cleanup write failed after a + // failed teardown. The flush re-inserts each buffered record, so a durable + // `pending_cleanup` row lands once the database recovers. The flush runs + // before the read below, so this same tick tears down a freshly-landed row. + try { + const flushed = await environmentRuntime.flushDeferredOrphanCleanups?.(); + if (flushed && (flushed.recovered > 0 || flushed.pending > 0)) { + logger.info( + { recovered: flushed.recovered, pending: flushed.pending }, + "flushed the in-process orphan sandbox cleanup buffer to the database", + ); + } + } catch { + // A flush failure never stops the sweep. The buffer keeps the orphan for a + // later tick, and the database rows below still need this sweep. The caught + // exception never enters the log, because a write error can carry a + // credential in its message, code, cause, or stack. + logger.warn("orphan sandbox cleanup buffer flush failed; the sweep continues"); + } + const rows = await db .select() .from(environmentLeases) @@ -13310,28 +13353,94 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) continue; } - const environment = await environmentsSvc.getById(row.environmentId); + const environment = row.environmentId + ? await environmentsSvc.getById(row.environmentId) + : null; const lease = await environmentsSvc.getLeaseById(row.id); - if (!environment || !lease) continue; + if (!lease) continue; + + // An orphan ephemeral lease keeps its provider, its provider lease id, and + // its sandbox config in the lease row. A failed acquire records it, and its + // environment row may be gone or foreign-bound. A reuse_by_environment lease + // whose environment a delete removed keeps the same recorded data, because + // the schema sets the environment reference to null on delete and preserves + // the row. Both leases tear down from the recorded lease data through + // `retryPendingSandboxTeardown`, which never reads the environment row. So + // the sweep uses that path whenever the lease is an orphan ephemeral lease + // or its environment row is gone. A reuse_by_environment lease whose + // environment still exists tears down through `destroyRunLease`. That + // path uses the provider and configuration recorded on the lease first; + // the environment is lifecycle context and only a legacy fallback. + const isOrphanEphemeralLease = lease.leasePolicy === "ephemeral"; + const useRecordedTeardown = isOrphanEphemeralLease || !environment; + + // Do not consume a finite cleanup attempt while the provider plugin is + // briefly unavailable. A plugin worker restart, a plugin reload, or a + // plugin reinstall makes the provider unavailable for a short window. The + // plugin can be missing or not ready in that window. A teardown then throws, + // and the atomic claim below would count that throw against the cap, so a + // long restart or reload could exhaust the retries and strand a live + // sandbox. So probe the provider first, and defer the lease this tick when + // the provider is not ready. The sweep preserves the pending_cleanup row, + // and a later sweep retries after the provider recovers. The probe reports + // ready only for a permanent condition (a missing provider string, a + // built-in provider, or no worker manager), so a genuine teardown failure + // still runs, throws, and counts toward the cap. A runtime with no probe + // method treats the lease as ready, so the sweep keeps its earlier + // behavior. + const workerReady = environmentRuntime.isPendingCleanupWorkerReady + ? await environmentRuntime.isPendingCleanupWorkerReady({ environment, lease }) + : true; + if (!workerReady) { + // Move the unavailable lease to the back of the sweep queue. Otherwise + // the oldest unavailable rows refill the page on every tick and starve a + // newer lease that has a ready provider. The defer bumps `updatedAt` + // only, so it consumes no finite retry attempt. + await deferPendingCleanupLease(row.id); + continue; + } // Atomically claim the attempt before the retry. Only the winning sweep - // increments the count and destroys the lease, so an overlapping sweep - // never destroys the same lease twice or exceeds the attempt cap. The + // increments the count and tears the sandbox down, so an overlapping sweep + // never tears the same sandbox down twice or exceeds the attempt cap. The // claim records the attempt before the retry, so a thrown driver error // still counts against the cap. const claimed = await claimPendingCleanupRetryAttempt(row.id, attempts); if (!claimed) continue; try { - const result = await environmentRuntime.destroyRunLease({ - environment, - lease, - failureReason: "pending_cleanup_retry", - }); - if (result && result.status !== "pending_cleanup") { + if (useRecordedTeardown) { + // Tear the sandbox down from the recorded provider config and the + // cleanup-authorized secret versions. The teardown returns no value + // and throws on failure, so the sweep releases the lease itself. + await environmentRuntime.retryPendingSandboxTeardown({ environment, lease }); + await environmentsSvc.releaseLease(lease.id, "expired", { + cleanupStatus: "success", + failureReason: "pending_cleanup_retry", + }); destroyed += 1; + } else if (environment) { + const result = await environmentRuntime.destroyRunLease({ + environment, + lease, + failureReason: "pending_cleanup_retry", + }); + if (result && result.status !== "pending_cleanup") { + destroyed += 1; + } } } catch { + // The recorded-data teardown throws on failure, so revert the lease to + // pending_cleanup for a later sweep. The claimed attempt still counts + // against the cap, so the retries stay bounded. The `destroyRunLease` + // path reverts the lease itself, so this revert only runs for the + // recorded-data teardown path. + if (useRecordedTeardown) { + await environmentsSvc.releaseLease(lease.id, "pending_cleanup", { + cleanupStatus: "failed", + failureReason: "pending_cleanup_retry", + }); + } // Log a constant errorKind only. The exception can carry a credential in // its name, code, message, cause, or stack, so the sweep never reads it. logger.warn( diff --git a/server/src/services/plugin-environment-driver.ts b/server/src/services/plugin-environment-driver.ts index 19789984e7..3ad4d7ef21 100644 --- a/server/src/services/plugin-environment-driver.ts +++ b/server/src/services/plugin-environment-driver.ts @@ -78,6 +78,7 @@ export interface ReadyPluginEnvironmentDriver { templateRefKind?: PluginEnvironmentDriverDeclaration["templateRefKind"]; templateConfigBinding?: PluginEnvironmentDriverDeclaration["templateConfigBinding"]; supportsTemplateDelete?: PluginEnvironmentDriverDeclaration["supportsTemplateDelete"]; + supportsSetupTokenLogin?: PluginEnvironmentDriverDeclaration["supportsSetupTokenLogin"]; } export function pluginDriverProviderKey(config: Pick): string { @@ -256,6 +257,7 @@ export async function listReadyPluginEnvironmentDrivers(input: { templateRefKind: driver.templateRefKind, templateConfigBinding: driver.templateConfigBinding, supportsTemplateDelete: driver.supportsTemplateDelete, + supportsSetupTokenLogin: driver.supportsSetupTokenLogin, })), ); } diff --git a/server/src/services/plugin-worker-manager.ts b/server/src/services/plugin-worker-manager.ts index 28d152541e..5006a9c5c6 100644 --- a/server/src/services/plugin-worker-manager.ts +++ b/server/src/services/plugin-worker-manager.ts @@ -37,6 +37,8 @@ import { isJsonRpcSuccessResponse, JsonRpcParseError, JsonRpcCallError, + SETUP_TOKEN_PTY_OUTPUT_NOTIFICATION, + SETUP_TOKEN_PTY_EXIT_NOTIFICATION, } from "@paperclipai/plugin-sdk"; import type { JsonRpcId, @@ -53,6 +55,7 @@ import type { InitializeParams, } from "@paperclipai/plugin-sdk"; import { getActiveStepContext } from "@paperclipai/adapter-utils/acpx-engine/startup-timing"; +import { CLAUDE_SETUP_TOKEN_COMMAND } from "@paperclipai/adapter-claude-local/server"; import { logger } from "../middleware/logger.js"; import { traceparentFromContextToken } from "../instrumentation.js"; @@ -133,6 +136,26 @@ const MAX_WORKER_MESSAGE_CHARS = 128 * 1024 * 1024; */ const MAX_EXECUTE_LOG_TOTAL_CHARS = 128 * 1024 * 1024; +/** Maximum characters for one live login pseudo-terminal output notification. */ +const MAX_SETUP_TOKEN_PTY_CHUNK_CHARS = 1_000_000; +/** Maximum cumulative output characters for one login pseudo-terminal route. */ +const MAX_SETUP_TOKEN_PTY_TOTAL_CHARS = 8 * 1024 * 1024; +/** The default open timeout for one login pseudo-terminal route, in milliseconds. */ +const SETUP_TOKEN_PTY_OPEN_TIMEOUT_MS = 30_000; +/** The default close timeout for one login pseudo-terminal route, in milliseconds. */ +const SETUP_TOKEN_PTY_CLOSE_TIMEOUT_MS = 10_000; +/** + * The fixed non-secret error a disallowed login command returns. The manager + * forwards only the compile-time `CLAUDE_SETUP_TOKEN_COMMAND` to the worker + * pseudo-terminal. It rejects any other command before the worker call, so a + * future caller cannot spawn an arbitrary process in the sandbox. + */ +const SETUP_TOKEN_PTY_COMMAND_NOT_ALLOWED = "SETUP_TOKEN_PTY_COMMAND_NOT_ALLOWED"; +/** The fixed non-secret error a rejected second credential open returns. */ +const SETUP_TOKEN_PTY_ROUTE_BUSY = "SETUP_TOKEN_PTY_ROUTE_BUSY"; +/** The fixed non-secret error a failed open returns. */ +const SETUP_TOKEN_PTY_OPEN_FAILED = "SETUP_TOKEN_PTY_OPEN_FAILED"; + /** Minimum time between two dropped-`execute.log` debug records. The router * rate-limits the record so a flood of dropped chunks writes at most one line * per window with a running count. */ @@ -282,6 +305,23 @@ export interface WorkerStartOptions { /** Max total characters one execute call may stream through `execute.log`. */ maxTotalCharsPerExecute?: number; }; + + /** + * Bounds and timeouts for the login pseudo-terminal route. The + * defaults bound one output notification, the cumulative output per route, and + * the open and the close timeouts. A test overrides them to exercise the + * terminalize paths without huge inputs or long waits. + */ + setupTokenPtyLimits?: { + /** Max characters for one login pseudo-terminal output notification. */ + maxChunkChars?: number; + /** Max cumulative output characters for one login pseudo-terminal route. */ + maxTotalChars?: number; + /** The open timeout for one login pseudo-terminal route, in milliseconds. */ + openTimeoutMs?: number; + /** The close timeout for one login pseudo-terminal route, in milliseconds. */ + closeTimeoutMs?: number; + }; } /** @@ -322,6 +362,37 @@ export type ExecuteLogSink = ( chunk: string, ) => void | Promise; +/** + * The input the manager needs to open one live login pseudo-terminal route + * The manager mints the host route identifier; the caller supplies + * only the sandbox scope, the provider lease id, and the fixed command. + */ +export interface SetupTokenPtyOpenInput { + driverKey: string; + companyId: string; + environmentId: string; + providerLeaseId: string; + command: string; +} + +/** + * One live login pseudo-terminal session the manager hands to the login + * transport. The shape matches the sandbox provider setup-token + * pseudo-terminal session, so the transport consumes it with no adapter. + */ +export interface SetupTokenPtyHostSession { + /** Registers the one output listener. The session streams each raw chunk in order. */ + onData(listener: (chunk: string) => void): void; + /** Writes raw input bytes to the pseudo-terminal. */ + write(data: string): void; + /** Resolves with the child exit code when the command ends or the route terminalizes. */ + wait(): Promise<{ exitCode: number | null }>; + /** Stops the child process. Safe to call more than one time. */ + kill(): void; + /** Closes the route and releases the terminal. Safe to call more than one time. */ + close(): Promise; +} + /** * Host-owned route for one active execute call. The host mints the invocation * id and stores the exact company id and log sink here. A worker never selects @@ -403,6 +474,16 @@ export interface PluginWorkerHandle { */ notify(method: string, params: unknown): void; + /** + * Open one live login pseudo-terminal route on this worker. The + * manager mints the host route identifier, reserves the route, drives the open, + * binds the worker session identifier one time, and returns a session the login + * transport drives. It permits one active credential pseudo-terminal per worker. + */ + openSetupTokenPtySession( + input: SetupTokenPtyOpenInput, + ): Promise; + /** * Authorize the set of companies this worker may act on from proactive * (non-invocation) context. Replaces any previously-authorized set. See the @@ -506,6 +587,17 @@ export interface PluginWorkerManager { timeoutMs?: number, executeLogSink?: ExecuteLogSink, ): Promise; + + /** + * Open one live login pseudo-terminal route on a specific plugin worker + * See {@link PluginWorkerHandle.openSetupTokenPtySession}. + * + * @throws if the worker is not registered. + */ + openSetupTokenPtySession( + pluginId: string, + input: SetupTokenPtyOpenInput, + ): Promise; } // --------------------------------------------------------------------------- @@ -564,6 +656,17 @@ export function createPluginWorkerHandle( const maxExecuteLogTotalChars = options.executeLogLimits?.maxTotalCharsPerExecute ?? MAX_EXECUTE_LOG_TOTAL_CHARS; + // Bounds and timeouts for the login pseudo-terminal route. A caller + // (a test) can lower them to exercise the terminalize paths. + const maxSetupTokenPtyChunkChars = + options.setupTokenPtyLimits?.maxChunkChars ?? MAX_SETUP_TOKEN_PTY_CHUNK_CHARS; + const maxSetupTokenPtyTotalChars = + options.setupTokenPtyLimits?.maxTotalChars ?? MAX_SETUP_TOKEN_PTY_TOTAL_CHARS; + const setupTokenPtyOpenTimeoutMs = + options.setupTokenPtyLimits?.openTimeoutMs ?? SETUP_TOKEN_PTY_OPEN_TIMEOUT_MS; + const setupTokenPtyCloseTimeoutMs = + options.setupTokenPtyLimits?.closeTimeoutMs ?? SETUP_TOKEN_PTY_CLOSE_TIMEOUT_MS; + // ------------------------------------------------------------------ // Proactive company scopes (LOOA-629) // ------------------------------------------------------------------ @@ -920,6 +1023,251 @@ export function createPluginWorkerHandle( } } + // ----------------------------------------------------------------------- + // Host-owned setup-token login pseudo-terminal route gate + // ----------------------------------------------------------------------- + // The manager owns one live login pseudo-terminal route per worker. It mints a + // host-owned opaque route identifier, carries it in the open call, and keys the + // close on it, so it closes a worker-created terminal even when the open reply + // was lost and no worker session identifier arrived. It binds the worker + // session identifier one time while the route is `opening`, for output only. It + // never trusts a worker-supplied identifier as proof of origin: it delivers + // output only while the route is `open` and the notification carries the exact + // bound identifier and valid bounded bytes, and it never logs the raw bytes. It + // terminalizes the route exactly once on every open failure path, closes the + // terminal by the host route identifier, and admits a new open only after it + // verifies a close acknowledgement bound to that identifier; it retires the + // worker on an unconfirmed close. + + type SetupTokenPtyRouteState = "reserved" | "opening" | "open" | "closed"; + interface SetupTokenPtyRoute { + hostRouteId: string; + state: SetupTokenPtyRouteState; + workerSessionId: string | null; + listener: ((chunk: string) => void) | null; + buffered: string[]; + deliveredChars: number; + terminalized: boolean; + settleWait: (value: { exitCode: number | null }) => void; + } + // At most one active credential pseudo-terminal per worker. A non-null route + // blocks a second open until the manager confirms the first route's close. + let setupTokenPtyRoute: SetupTokenPtyRoute | null = null; + + function settleSetupTokenPtyWait( + route: SetupTokenPtyRoute, + value: { exitCode: number | null }, + ): void { + const settle = route.settleWait; + route.settleWait = () => {}; + settle(value); + } + + // Close the worker terminal by the host route identifier and verify the bound + // acknowledgement. Return true only when the worker returns an acknowledgement + // that carries the exact host route identifier. An absent, malformed, + // mismatched, or timed-out acknowledgement returns false, so the caller fails + // closed. + async function closeSetupTokenPtyTerminal(hostRouteId: string): Promise { + try { + const ack = await callInternal( + "setupTokenPtyClose", + { hostRouteId }, + setupTokenPtyCloseTimeoutMs, + ); + return isRecord(ack) && readNonEmptyString(ack.hostRouteId) === hostRouteId; + } catch { + return false; + } + } + + // Terminalize the route exactly once. Resolve the login wait, close the worker + // terminal by the host route identifier, and free the per-worker slot only + // after the close resolves. Retire the worker when the close is unconfirmed. + async function terminalizeSetupTokenPtyRoute(route: SetupTokenPtyRoute): Promise { + if (route.terminalized) return; + route.terminalized = true; + route.state = "closed"; + route.listener = null; + route.buffered = []; + // A terminalized route reports a null exit code, which the runner treats as a + // failure. + settleSetupTokenPtyWait(route, { exitCode: null }); + const confirmed = await closeSetupTokenPtyTerminal(route.hostRouteId); + if (setupTokenPtyRoute === route) setupTokenPtyRoute = null; + if (!confirmed) { + // The worker did not acknowledge the close, so the host cannot prove the + // terminal is gone. Fail closed: retire the worker before any reuse. + log.error( + { pluginId }, + "setup-token login pseudo-terminal close not acknowledged; retiring worker", + ); + void killProcess(); + } + } + + // Route one login pseudo-terminal output notification to the per-session + // listener. Deliver only while the route is `open` and the notification carries + // the exact bound worker session identifier and valid bounded bytes. Drop an + // unknown, late, malformed, or mismatched notification. Never log the raw bytes. + function routeSetupTokenPtyOutput(notification: JsonRpcNotification): void { + const route = setupTokenPtyRoute; + if (!route || route.state !== "open") return; + const params = isRecord(notification.params) ? notification.params : {}; + const workerSessionId = readNonEmptyString(params.workerSessionId); + if (!workerSessionId || workerSessionId !== route.workerSessionId) return; + const chunk = params.chunk; + if ( + typeof chunk !== "string" || + chunk.length === 0 || + chunk.length > maxSetupTokenPtyChunkChars + ) { + return; + } + if (route.deliveredChars + chunk.length > maxSetupTokenPtyTotalChars) { + // The cumulative output passed the per-route bound. Terminalize the route. + void terminalizeSetupTokenPtyRoute(route); + return; + } + route.deliveredChars += chunk.length; + if (route.listener) route.listener(chunk); + else route.buffered.push(chunk); + } + + // Route one login pseudo-terminal exit notification to the login wait. Resolve + // only while the route is `open` and the notification carries the exact bound + // worker session identifier. + function routeSetupTokenPtyExit(notification: JsonRpcNotification): void { + const route = setupTokenPtyRoute; + if (!route || route.state !== "open") return; + const params = isRecord(notification.params) ? notification.params : {}; + const workerSessionId = readNonEmptyString(params.workerSessionId); + if (!workerSessionId || workerSessionId !== route.workerSessionId) return; + const exitCode = typeof params.exitCode === "number" ? params.exitCode : null; + settleSetupTokenPtyWait(route, { exitCode }); + } + + // Close the one route on a worker exit. The worker is gone, so the manager + // resolves the login wait with the fixed non-secret exit and clears the route + // one time. The pending pseudo-terminal calls reject through `rejectAllPending`. + function closeSetupTokenPtyRouteOnWorkerExit(): void { + const route = setupTokenPtyRoute; + if (!route) return; + setupTokenPtyRoute = null; + route.terminalized = true; + route.state = "closed"; + route.listener = null; + route.buffered = []; + settleSetupTokenPtyWait(route, { exitCode: null }); + } + + // Open one live login pseudo-terminal route. Reserve the route + // before the open call, bind the worker session identifier one time on the + // first successful open reply, and return a session the login transport drives. + // Terminalize the route on every open failure path. + async function openSetupTokenPtySession( + input: SetupTokenPtyOpenInput, + ): Promise { + if (input.command !== CLAUDE_SETUP_TOKEN_COMMAND) { + // Allowlist the login command. Only the fixed `CLAUDE_SETUP_TOKEN_COMMAND` + // may run in the sandbox pseudo-terminal. Reject any other command with one + // fixed non-secret error before the worker call, so a caller cannot spawn + // an arbitrary process in the sandbox pseudo-terminal. + throw new Error(SETUP_TOKEN_PTY_COMMAND_NOT_ALLOWED); + } + if (setupTokenPtyRoute) { + // A route for this worker is not yet closed and confirmed. Reject the + // second open with one fixed non-secret error before it reaches the worker. + throw new Error(SETUP_TOKEN_PTY_ROUTE_BUSY); + } + const hostRouteId = randomUUID(); + let settleWait: (value: { exitCode: number | null }) => void = () => {}; + const waitPromise = new Promise<{ exitCode: number | null }>((resolve) => { + settleWait = resolve; + }); + const route: SetupTokenPtyRoute = { + hostRouteId, + state: "reserved", + workerSessionId: null, + listener: null, + buffered: [], + deliveredChars: 0, + terminalized: false, + settleWait, + }; + setupTokenPtyRoute = route; + + route.state = "opening"; + let openResult: HostToWorkerMethods["setupTokenPtyOpen"][1]; + try { + openResult = await callInternal( + "setupTokenPtyOpen", + { + hostRouteId, + driverKey: input.driverKey, + companyId: input.companyId, + environmentId: input.environmentId, + providerLeaseId: input.providerLeaseId, + command: input.command, + }, + setupTokenPtyOpenTimeoutMs, + ); + } catch (err) { + // A send failure, an RPC rejection, or an open timeout. Terminalize the + // route exactly once and fail closed. + await terminalizeSetupTokenPtyRoute(route); + throw err instanceof Error ? err : new Error(SETUP_TOKEN_PTY_OPEN_FAILED); + } + + const workerSessionId = readNonEmptyString( + isRecord(openResult) ? openResult.workerSessionId : null, + ); + if (!workerSessionId || route.state !== "opening" || route.terminalized) { + // A malformed reply, or a route that already left `opening`. A late or a + // duplicate reply never binds, revives, or reopens a route. + await terminalizeSetupTokenPtyRoute(route); + throw new Error(SETUP_TOKEN_PTY_OPEN_FAILED); + } + // Bind the worker session identifier one time and move the route to `open`. + route.workerSessionId = workerSessionId; + route.state = "open"; + + return { + onData(listener: (chunk: string) => void): void { + route.listener = listener; + if (route.buffered.length > 0) { + const pending = route.buffered; + route.buffered = []; + for (const chunk of pending) listener(chunk); + } + }, + write(data: string): void { + const sid = route.workerSessionId; + if (route.state !== "open" || !sid) return; + void callInternal( + "setupTokenPtyInput", + { workerSessionId: sid, data }, + setupTokenPtyOpenTimeoutMs, + ).catch(() => {}); + }, + wait(): Promise<{ exitCode: number | null }> { + return waitPromise; + }, + kill(): void { + const sid = route.workerSessionId; + if (!sid) return; + void callInternal( + "setupTokenPtyStop", + { workerSessionId: sid }, + setupTokenPtyOpenTimeoutMs, + ).catch(() => {}); + }, + async close(): Promise { + await terminalizeSetupTokenPtyRoute(route); + }, + }; + } + /** * Extract the single company a worker→host call references, mirroring the SDK * governed-access gate's own derivation (host-client-factory.ts @@ -1080,6 +1428,18 @@ export function createPluginWorkerHandle( return; } + // Setup-token login pseudo-terminal notifications: deliver output + // and the exit to the one host-owned login route, bound by the worker session + // identifier while the route is open. + if (notification.method === SETUP_TOKEN_PTY_OUTPUT_NOTIFICATION) { + routeSetupTokenPtyOutput(notification); + return; + } + if (notification.method === SETUP_TOKEN_PTY_EXIT_NOTIFICATION) { + routeSetupTokenPtyExit(notification); + return; + } + // Stream notifications: forward to the stream bus via callback if ( notification.method === "streams.open" || @@ -1225,6 +1585,11 @@ export function createPluginWorkerHandle( )), ); + // Close the one login pseudo-terminal route with a fixed non-secret exit and + // clear the route one time. The pending pseudo-terminal calls + // already rejected through `rejectAllPending`. + closeSetupTokenPtyRouteOnWorkerExit(); + // Emit synthetic close for any orphaned stream channels so SSE clients // are notified instead of hanging indefinitely. if (openStreamChannels.size > 0 && options.onStreamNotification) { @@ -1687,6 +2052,17 @@ export function createPluginWorkerHandle( return callInternal(method, params, timeoutMs, executeLogSink); }, + openSetupTokenPtySession(input: SetupTokenPtyOpenInput) { + if (status !== "running" && status !== "starting") { + return Promise.reject( + new Error( + `Cannot open a login pseudo-terminal — worker for "${pluginId}" is ${status}`, + ), + ); + } + return openSetupTokenPtySession(input); + }, + notify(method: string, params: unknown) { if (status !== "running") return; const invocationScope = deriveInvocationScope(method, params); @@ -1925,5 +2301,15 @@ export function createPluginWorkerManager( } return handle.call(method, params, timeoutMs, executeLogSink); }, + + openSetupTokenPtySession(pluginId: string, input: SetupTokenPtyOpenInput) { + const handle = workers.get(pluginId); + if (!handle) { + return Promise.reject( + new Error(`No worker registered for plugin "${pluginId}"`), + ); + } + return handle.openSetupTokenPtySession(input); + }, }; } diff --git a/server/src/services/sandbox-orphan-cleanup-spool.ts b/server/src/services/sandbox-orphan-cleanup-spool.ts new file mode 100644 index 0000000000..a1729735ed --- /dev/null +++ b/server/src/services/sandbox-orphan-cleanup-spool.ts @@ -0,0 +1,198 @@ +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { createHash } from "node:crypto"; +import { resolvePaperclipInstanceRoot } from "../home-paths.js"; + +/** + * One orphan pending-cleanup record. The acquire builds it when the conditional + * lease insert rejects a live sandbox. The record carries the same fields a + * `pending_cleanup` lease row needs, so a later flush re-inserts it without + * loss. The `metadata` field holds the sanitized lease metadata: it carries + * secret references, not secret values, so the record is safe to persist next to + * the other durable local state. A restart-recovered record still authorizes the + * teardown, because it keeps the immutable provider, provider lease id, and + * recorded config the teardown needs. + */ +export interface DeferredOrphanCleanupRecord { + companyId: string; + environmentId: string; + executionWorkspaceId: string | null; + issueId: string | null; + heartbeatRunId: string | null; + provider: string; + providerLeaseId: string | null; + metadata: Record; +} + +/** + * A durable, restart-safe store for orphan pending-cleanup records. The acquire + * appends a record here when the conditional lease insert rejects a live + * sandbox, the compensating teardown fails, and every synchronous + * `pending_cleanup` database write also fails. The database is the only + * automated way to find a leaked sandbox, but the database is down at that + * instant, so the record has nowhere durable to land. The in-process buffer + * alone loses the record on a restart. This spool keeps the record on the local + * disk, next to the other durable server state, so a restart still finds it. A + * later cleanup sweep loads the spool, re-inserts each record as a durable + * `pending_cleanup` row, and removes the spooled copy. + */ +export interface SandboxOrphanCleanupSpool { + /** + * Persist one orphan record durably. Report `true` when the record reached the + * disk, or `false` on any failure, so the caller keeps the in-process buffer + * and the error log as the fallback handles. This method never throws. + */ + append(record: DeferredOrphanCleanupRecord): Promise; + /** + * Load every persisted orphan record. The cleanup sweep calls this once after + * a restart, so the records a previous process could not land re-enter the + * flush path. This method never throws; it returns an empty list on any read + * failure and skips a malformed file. + */ + load(): Promise; + /** + * Remove one persisted orphan record after its durable `pending_cleanup` row + * lands. The removal runs after the database insert succeeds, so a crash + * between the insert and this removal leaves the spooled copy for a later + * flush. That later flush re-inserts a duplicate row, which the idempotent + * teardown handles. This order never loses the orphan. This method never + * throws. + */ + remove(record: DeferredOrphanCleanupRecord): Promise; +} + +// The spool writes one file per orphan record, so a removal never rewrites a +// shared file and never races a concurrent append. The file name is a stable +// hash of the record identity, so a repeated append for the same orphan +// overwrites the same file and never persists a duplicate. +const RECORD_FILE_SUFFIX = ".json"; + +// A unique temp-file counter, so two concurrent appends for two different +// records never share a temp path. The atomic rename then publishes each record +// without a torn write. +let tempFileCounter = 0; + +function resolveDefaultSpoolDir(): string { + return ( + process.env.SANDBOX_ORPHAN_CLEANUP_SPOOL_DIR ?? + path.resolve(resolvePaperclipInstanceRoot(), "data", "sandbox-orphan-cleanup") + ); +} + +// Build the stable file name for one record. Key on the provider lease id, the +// value the teardown needs, so a repeated append for the same orphan reuses the +// same file. A null provider lease id carries no identity, so fall back to a +// hash of the whole record. The hash keeps the name on the safe file-name +// charset and hides the raw identifiers from a directory listing. +function recordFileName(record: DeferredOrphanCleanupRecord): string { + const identity = + record.providerLeaseId !== null + ? `lease:${record.providerLeaseId}` + : `record:${JSON.stringify(record)}`; + const hash = createHash("sha256").update(identity).digest("hex"); + return `${hash}${RECORD_FILE_SUFFIX}`; +} + +function isDeferredOrphanCleanupRecord(value: unknown): value is DeferredOrphanCleanupRecord { + if (typeof value !== "object" || value === null) return false; + const candidate = value as Record; + const stringOrNull = (field: unknown) => field === null || typeof field === "string"; + return ( + typeof candidate.companyId === "string" && + typeof candidate.environmentId === "string" && + stringOrNull(candidate.executionWorkspaceId) && + stringOrNull(candidate.issueId) && + stringOrNull(candidate.heartbeatRunId) && + typeof candidate.provider === "string" && + stringOrNull(candidate.providerLeaseId) && + typeof candidate.metadata === "object" && + candidate.metadata !== null + ); +} + +// Write one file durably. Write the bytes to a unique temp file, flush the file +// to the disk, rename it onto the final path, then flush the directory. The +// rename is atomic on POSIX, so a reader never sees a torn record, and a crash +// after the directory flush still leaves the whole record on the disk. +async function writeFileDurable(absPath: string, data: string): Promise { + tempFileCounter += 1; + const tempPath = `${absPath}.${process.pid}.${tempFileCounter}.tmp`; + const handle = await fs.open(tempPath, "w"); + try { + await handle.writeFile(data, "utf8"); + await handle.sync(); + } finally { + await handle.close(); + } + try { + await fs.rename(tempPath, absPath); + } catch (error) { + // The rename failed, so drop the temp file. Do not leave it behind, or a + // later load reads a half-published record. + await fs.rm(tempPath, { force: true }).catch(() => undefined); + throw error; + } + const dirHandle = await fs.open(path.dirname(absPath), "r"); + try { + await dirHandle.sync(); + } finally { + await dirHandle.close(); + } +} + +/** + * Build a local-disk orphan-cleanup spool. The spool writes one file per record + * under the spool directory. The default directory sits next to the other + * durable server state under the instance root. A caller can override the + * directory for a test. + */ +export function createSandboxOrphanCleanupSpool(spoolDir?: string): SandboxOrphanCleanupSpool { + const dir = spoolDir ?? resolveDefaultSpoolDir(); + + return { + async append(record) { + const absPath = path.join(dir, recordFileName(record)); + try { + await fs.mkdir(dir, { recursive: true }); + // Serialize the record only. A caught write error never enters a log, + // because it can carry a credential in its message, code, cause, or + // stack. So this method reports a boolean and never logs. + await writeFileDurable(absPath, JSON.stringify(record)); + return true; + } catch { + return false; + } + }, + + async load() { + let names: string[]; + try { + names = await fs.readdir(dir); + } catch { + // The directory does not exist yet, or the read failed. Either way, no + // record waits on the disk. + return []; + } + const records: DeferredOrphanCleanupRecord[] = []; + for (const name of names) { + if (!name.endsWith(RECORD_FILE_SUFFIX)) continue; + try { + const raw = await fs.readFile(path.join(dir, name), "utf8"); + const parsed: unknown = JSON.parse(raw); + if (isDeferredOrphanCleanupRecord(parsed)) { + records.push(parsed); + } + } catch { + // Skip a malformed or unreadable file. A partial file cannot authorize + // a teardown, so the load ignores it and keeps the other records. + } + } + return records; + }, + + async remove(record) { + const absPath = path.join(dir, recordFileName(record)); + await fs.rm(absPath, { force: true }).catch(() => undefined); + }, + }; +} diff --git a/server/src/services/sandbox-provider-runtime.ts b/server/src/services/sandbox-provider-runtime.ts index 0b089fdd6f..74566c3ea5 100644 --- a/server/src/services/sandbox-provider-runtime.ts +++ b/server/src/services/sandbox-provider-runtime.ts @@ -20,6 +20,14 @@ export interface AcquireSandboxLeaseInput { issueId: string | null; agentId?: string | null; executionWorkspaceId?: string | null; + /** + * The absolute latest time the acquired lease may stay active, as an ISO 8601 + * timestamp. A caller with an independent deadline sets it. The provider must + * configure a provider-side expiry at or before this time, and return the real + * provider expiry in `SandboxLeaseHandle.expiresAt`. When omitted, the provider + * keeps its default lifetime. + */ + requestedExpiresAt?: string | null; } export interface ResumeSandboxLeaseInput { @@ -63,6 +71,12 @@ export interface SandboxExecuteInput { export interface SandboxLeaseHandle { providerLeaseId: string; metadata: Record; + /** + * The real provider-side expiry the provider granted, as an ISO 8601 timestamp. + * A provider that honors a requested deadline returns the actual expiry it + * configured. Absent when the provider granted no bounded lifetime. + */ + expiresAt?: string | null; } export interface PreparedSandboxWorkspace { @@ -336,6 +350,7 @@ export async function acquireSandboxProviderLease(input: { agentId?: string | null; executionWorkspaceId?: string | null; reusableProviderLeaseId?: string | null; + requestedExpiresAt?: string | null; }): Promise { const provider = requireSandboxProvider(input.config.provider); if (provider.supportsReusableLeases && input.config.reuseLease && input.reusableProviderLeaseId) { @@ -355,6 +370,7 @@ export async function acquireSandboxProviderLease(input: { issueId: input.issueId, agentId: input.agentId, executionWorkspaceId: input.executionWorkspaceId, + requestedExpiresAt: input.requestedExpiresAt, }); } diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 20bec111a7..fada3eca4c 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -68,6 +68,10 @@ import { logActivity } from "./activity-log.js"; const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; const AGENT_ACCESS_CONFIG_PATH_PREFIX = "access."; +// System consumer id for a durable orphan-sandbox teardown. The cleanup sweep +// resolves the recorded connection secret under this id so the audit trail +// marks the read as an orphan-sandbox teardown, not a normal environment read. +export const SANDBOX_CLEANUP_CONSUMER_ID = "environment-sandbox-cleanup"; const SENSITIVE_ENV_KEY_RE = /(api[-_]?key|access[-_]?token|auth(?:_?token)?|authorization|bearer|secret|passwd|password|credential|jwt|private[-_]?key|cookie|connectionstring)/i; const REDACTED_SENTINEL = "***REDACTED***"; @@ -80,6 +84,180 @@ const FALLBACK_ADAPTER_SCHEMA_SECRET_FIELDS: Readonly { + if (typeof config !== "object" || config === null || Array.isArray(config)) return {}; + const env = (config as Record).env; + if (typeof env !== "object" || env === null || Array.isArray(env)) return {}; + return env as Record; +} + +/** + * Returns true when a binding is the exact fixed Claude Code OAuth user-secret + * reference. The fixed binding is a `user_secret_ref` whose key is the fixed + * key. Any other shape (a plain value, a company secret reference, or a + * different user-secret key) is a replacement or a weaker binding. + */ +export function isFixedClaudeOAuthBinding(binding: unknown): boolean { + if (typeof binding !== "object" || binding === null) return false; + const record = binding as Record; + return record.type === "user_secret_ref" && record.key === CLAUDE_CODE_OAUTH_TOKEN_KEY; +} + +/** True when the config carries the exact fixed OAuth binding. */ +function hasFixedClaudeOAuthBinding(config: unknown): boolean { + return isFixedClaudeOAuthBinding(readAdapterEnvRecord(config)[CLAUDE_CODE_OAUTH_TOKEN_KEY]); +} + +/** + * Returns true when the config delivers a non-empty ANTHROPIC_API_KEY. A plain + * binding counts only when it has a non-empty value. A company or user secret + * reference always counts, because it resolves to a value at runtime. + */ +function hasAnthropicApiKeyCredential(config: unknown): boolean { + const binding = readAdapterEnvRecord(config)[ANTHROPIC_API_KEY_ENV]; + if (typeof binding === "string") return binding.trim().length > 0; + if (typeof binding !== "object" || binding === null) return false; + const record = binding as Record; + if (record.type === "plain") { + return typeof record.value === "string" && record.value.trim().length > 0; + } + return record.type === "secret_ref" || record.type === "user_secret_ref"; +} + +export interface ClaudeOAuthBindingInvariantInput { + /** The effective adapter type of the write. */ + adapterType: string | null | undefined; + /** The normalized adapter config the write persists. */ + nextConfig: unknown; + /** The stored adapter config before the write. Null on a create. */ + priorConfig?: unknown; +} + +export interface ClaudeOAuthBindingInvariantDecision { + /** True when the write adds the fixed binding that the prior config lacked. */ + introducesBinding: boolean; + /** True when the write keeps an existing fixed binding unchanged. */ + keepsBinding: boolean; +} + +/** + * The Claude OAuth binding check. It runs after generic normalization and + * before every database write on a `claude_local` create, hire, update, + * approval activation, and configuration rollback path. + * + * The `CLAUDE_CODE_OAUTH_TOKEN` binding behaves like a normal environment + * variable. A normal write can remove the fixed binding, or re-point it to a + * plain value, a company-secret reference, or a different user-secret key. The + * function no longer locks a prior fixed binding against removal or replacement. + * + * A write to a non-claude_local adapter that has no prior fixed binding stays + * outside the Claude login flow. A prior fixed binding keeps the function active + * for the write, so the function still reports whether the write keeps that + * binding, independently of the destination adapter type. + * + * The precedence policy runs on every path: the fixed binding together with a + * non-empty ANTHROPIC_API_KEY is a conflict. The function rejects that conflict + * with a generic message that names no token value and no owner configuration. + * + * The function returns whether the write introduces the fixed binding or keeps + * an existing one. The create and hire paths consume a stored-session claim when + * the write introduces the binding. The update, approval, and rollback paths + * reject a newly introduced binding, because they carry no claim. + */ +export function assertClaudeOAuthBindingInvariant( + input: ClaudeOAuthBindingInvariantInput, +): ClaudeOAuthBindingInvariantDecision { + const isClaudeLocal = input.adapterType === CLAUDE_LOCAL_ADAPTER_TYPE; + const nextIsFixed = hasFixedClaudeOAuthBinding(input.nextConfig); + const priorIsFixed = hasFixedClaudeOAuthBinding(input.priorConfig); + + // A write to a non-claude_local adapter that has no prior fixed binding is a + // normal non-Claude configuration. It stays outside the Claude login flow. A + // prior fixed binding always keeps the function active, so a write still + // reports the binding state after a move to another adapter type. + if (!isClaudeLocal && !priorIsFixed) { + return { introducesBinding: false, keepsBinding: false }; + } + + if (nextIsFixed && hasAnthropicApiKeyCredential(input.nextConfig)) { + throw new HttpError(409, CLAUDE_OAUTH_CREDENTIAL_CONFLICT, { + code: "claude_oauth_credential_conflict", + }); + } + return { + introducesBinding: nextIsFixed && !priorIsFixed, + keepsBinding: nextIsFixed && priorIsFixed, + }; +} + +/** The fixed error the create and hire paths raise for a rejected claim. */ +export function claudeOAuthClaimRejectedError(): HttpError { + return new HttpError(409, CLAUDE_OAUTH_CLAIM_REJECTED, { code: "claude_oauth_claim_rejected" }); +} + type DbTransaction = Parameters[0]>[0]; type SecretBindingDb = Pick; @@ -624,7 +802,9 @@ function secretResolutionErrorCode(error: unknown): SecretResolutionErrorCode { if (error.message === "Responsible user is required for user secret resolution") { return "responsible_user_missing"; } - if (error.message === "User secret definition not found") return "user_secret_definition_missing"; + if (error.message.startsWith("User secret definition not found")) { + return "user_secret_definition_missing"; + } if (error.message === "User secret definition is not active") return "user_secret_definition_inactive"; if (error.message === "User-scoped secrets must be resolved through user secret declarations") { return "secret_scope_invalid"; @@ -682,6 +862,47 @@ function missingUserSecretDefinitionRuntimeBinding( }; } +// A direct-resolution path (Test or save) resolves one user-secret binding at a +// time. When the definition is gone, this builder names the environment +// variable, the consumer, and the unresolved definition so the actor can find +// the dangling binding. The message keeps the exact "User secret definition not +// found" prefix so `secretResolutionErrorCode` still maps it to +// `user_secret_definition_missing`. It never includes a secret value. +type UserSecretDefinitionResolutionContext = { + envKey?: string | null; + configPath?: string | null; + consumerType?: string | null; + consumerId?: string | null; +}; + +function envKeyFromConfigPath(configPath?: string | null): string | null { + if (!configPath) return null; + const separatorIndex = configPath.lastIndexOf("."); + const key = separatorIndex >= 0 ? configPath.slice(separatorIndex + 1) : configPath; + return key.length > 0 ? key : null; +} + +const BASE_USER_SECRET_DEFINITION_NOT_FOUND_MESSAGE = "User secret definition not found"; + +function userSecretDefinitionNotFoundMessage( + input: { definitionId?: string | null; definitionKey?: string | null }, + context?: UserSecretDefinitionResolutionContext, +): string { + const parts: string[] = []; + const envKey = context?.envKey ?? envKeyFromConfigPath(context?.configPath); + if (envKey) parts.push(`environment variable "${envKey}"`); + if (context?.consumerId) { + parts.push(`${context.consumerType ?? "consumer"} ${context.consumerId}`); + } + if (input.definitionKey) { + parts.push(`definition key "${input.definitionKey}"`); + } else if (input.definitionId) { + parts.push(`definition id "${input.definitionId}"`); + } + if (parts.length === 0) return BASE_USER_SECRET_DEFINITION_NOT_FOUND_MESSAGE; + return `${BASE_USER_SECRET_DEFINITION_NOT_FOUND_MESSAGE} for ${parts.join(", ")}`; +} + function assertSelectableProviderConfig(config: { provider: string; status: string; @@ -779,6 +1000,7 @@ export function secretService(db: Db) { companyId: string, input: { definitionId?: string | null; definitionKey?: string | null }, source: Pick = db, + context?: UserSecretDefinitionResolutionContext, ) { const definition = input.definitionId ? await getUserSecretDefinitionById(companyId, input.definitionId, source) @@ -786,7 +1008,7 @@ export function secretService(db: Db) { ? await getUserSecretDefinitionByKey(companyId, input.definitionKey, source) : null; if (!definition || definition.deletedAt || definition.status === "deleted") { - throw notFound("User secret definition not found"); + throw notFound(userSecretDefinitionNotFoundMessage(input, context)); } if (definition.companyId !== companyId) { throw unprocessable("User secret definition must belong to same company"); @@ -1248,6 +1470,44 @@ export function secretService(db: Db) { })).value; } + // Resolve a connection secret for a durable orphan-sandbox teardown. + // + // The retry destroys a remote sandbox that a failed acquire left allocated. + // The teardown needs the same connection secret the acquire used. But the + // environment binding may be gone: a delete removed the environment, or a + // provider change replaced the binding. So this path authorizes the read from + // the durable `pending_cleanup` lease row, not from the environment binding, + // and it never checks the binding. The caller passes only a secret id that the + // durable row recorded, so the scope stays narrow. The resolution records an + // access event for audit, and the value never enters lease metadata. + async function resolveSecretValueForSandboxCleanup( + companyId: string, + secretId: string, + version: number | "latest", + context: { + configPath: string; + issueId?: string | null; + heartbeatRunId?: string | null; + }, + ): Promise { + return ( + await resolveSecretValueInternal(companyId, secretId, version, { + // Audit-only access context. No `bindingContext`, so the resolver never + // asserts the environment binding that a delete or a provider change may + // have removed. + accessContext: { + consumerType: "system", + consumerId: SANDBOX_CLEANUP_CONSUMER_ID, + actorType: "system", + actorId: null, + configPath: context.configPath, + issueId: context.issueId ?? null, + heartbeatRunId: context.heartbeatRunId ?? null, + }, + }) + ).value; + } + async function resolveSecretValueForAgentAccess( companyId: string, secretId: string, @@ -2243,6 +2503,218 @@ export function secretService(db: Db) { } } + // --- Claude Code OAuth login: narrow definition and owner-bound write ------- + + type UserSecretDefinitionRow = typeof userSecretDefinitions.$inferSelect; + type CompanySecretRow = typeof companySecrets.$inferSelect; + + /** True only when a stored definition matches the fixed Claude OAuth shape. */ + function isCompatibleClaudeOAuthDefinition(definition: UserSecretDefinitionRow) { + return ( + definition.key === CLAUDE_CODE_OAUTH_DEFINITION.key && + definition.name === CLAUDE_CODE_OAUTH_DEFINITION.name && + definition.provider === CLAUDE_CODE_OAUTH_DEFINITION.provider && + definition.managedMode === CLAUDE_CODE_OAUTH_DEFINITION.managedMode && + definition.status === CLAUDE_CODE_OAUTH_DEFINITION.status + ); + } + + /** Reads the recorded setup-token session id from an owner value, or null. */ + function readClaudeOAuthSessionId(secret: CompanySecretRow): string | null { + const metadata = secret.providerMetadata; + if (!metadata || typeof metadata !== "object") return null; + const value = (metadata as Record)[CLAUDE_OAUTH_SESSION_METADATA_FIELD]; + return typeof value === "string" ? value : null; + } + + /** Records the setup-token session id on the owner value. Not a secret. */ + async function stampClaudeOAuthSessionId( + secret: CompanySecretRow, + sessionId: string, + ): Promise { + const nextMetadata: Record = { + ...(secret.providerMetadata ?? {}), + [CLAUDE_OAUTH_SESSION_METADATA_FIELD]: sessionId, + }; + return db + .update(companySecrets) + .set({ providerMetadata: nextMetadata, updatedAt: new Date() }) + .where(eq(companySecrets.id, secret.id)) + .returning() + .then((rows) => rows[0] ?? secret); + } + + function toClaudeOAuthResult(secret: CompanySecretRow): ClaudeOAuthUserSecretResult { + return { + secretId: secret.id, + latestVersion: secret.latestVersion, + definitionId: secret.userSecretDefinitionId ?? "", + }; + } + + /** + * Ensures the fixed Claude Code OAuth user-secret definition for a company. The + * helper accepts no key, name, provider, mode, or status from a caller. It + * reads the existing definition by the company and the fixed key. It returns an + * exact compatible definition. It rejects a conflicting definition with 409 and + * does not mutate it. After a uniqueness conflict it re-reads the row and + * compares the fixed fields before it returns. + */ + async function ensureClaudeOAuthUserSecretDefinitionInternal( + companyId: string, + actor?: { userId?: string | null; agentId?: string | null }, + ): Promise { + const existing = await getUserSecretDefinitionByKey(companyId, CLAUDE_CODE_OAUTH_DEFINITION.key); + if (existing) { + if (!isCompatibleClaudeOAuthDefinition(existing)) { + throw conflict(CLAUDE_OAUTH_DEFINITION_CONFLICT); + } + return existing; + } + try { + return await db + .insert(userSecretDefinitions) + .values({ + companyId, + key: CLAUDE_CODE_OAUTH_DEFINITION.key, + name: CLAUDE_CODE_OAUTH_DEFINITION.name, + description: null, + status: CLAUDE_CODE_OAUTH_DEFINITION.status, + provider: CLAUDE_CODE_OAUTH_DEFINITION.provider, + providerConfigId: null, + managedMode: CLAUDE_CODE_OAUTH_DEFINITION.managedMode, + providerMetadata: null, + usageGuidance: null, + createdByAgentId: actor?.agentId ?? null, + createdByUserId: actor?.userId ?? null, + updatedByAgentId: actor?.agentId ?? null, + updatedByUserId: actor?.userId ?? null, + }) + .returning() + .then((rows) => rows[0]); + } catch (error) { + if (isUniqueConstraintViolation(error, USER_SECRET_DEFINITION_KEY_UNIQUE_CONSTRAINT)) { + // A concurrent create won the race. Re-read and compare the fixed fields. + const raced = await getUserSecretDefinitionByKey(companyId, CLAUDE_CODE_OAUTH_DEFINITION.key); + if (raced && isCompatibleClaudeOAuthDefinition(raced)) return raced; + throw conflict(CLAUDE_OAUTH_DEFINITION_CONFLICT); + } + throw error; + } + } + + /** + * The owner-bound compare-and-set for the Claude Code OAuth value. It has two + * modes. `first_write` creates a value only when no owner value exists. + * `confirmed_rotation` rotates only after confirmation with the expected secret + * id and the expected latest version. The session id is the idempotency key: a + * repeated successful completion returns the stored result and creates no new + * version. + */ + async function completeClaudeOAuthUserSecretInternal( + companyId: string, + ownerUserId: string, + input: { + sessionId: string; + mode: "first_write" | "confirmed_rotation"; + value: string; + expectedSecretId?: string | null; + expectedLatestVersion?: number | null; + }, + actor?: { userId?: string | null; agentId?: string | null }, + ): Promise { + const sessionId = input.sessionId?.trim(); + if (!sessionId) throw unprocessable("A Claude login session id is required"); + const value = input.value?.trim(); + if (!value) throw unprocessable("A Claude login token value is required"); + + const definition = await ensureClaudeOAuthUserSecretDefinitionInternal(companyId, actor); + + // Idempotency: a prior successful completion for this session returns the + // stored result and creates no new version. + const existing = await getUserSecretValue({ companyId, ownerUserId, definitionId: definition.id }); + if (existing && readClaudeOAuthSessionId(existing) === sessionId) { + return toClaudeOAuthResult(existing); + } + + if (input.mode === "first_write") { + if (existing) throw conflict(CLAUDE_OAUTH_VALUE_EXISTS); + let created: CompanySecretRow; + try { + created = await createUserSecretValueInternal( + companyId, + ownerUserId, + { definitionId: definition.id, value }, + actor, + ); + } catch (error) { + // Two concurrent first writes race the partial unique index. Re-read and + // return the stored result only when this session already won. + if (error instanceof HttpError && error.status === 409) { + const raced = await getUserSecretValue({ companyId, ownerUserId, definitionId: definition.id }); + if (raced && readClaudeOAuthSessionId(raced) === sessionId) { + return toClaudeOAuthResult(raced); + } + } + throw error; + } + const stamped = await stampClaudeOAuthSessionId(created, sessionId); + return toClaudeOAuthResult(stamped); + } + + // confirmed_rotation. + if (!input.expectedSecretId || input.expectedLatestVersion == null) { + throw unprocessable("A confirmed rotation requires expectedSecretId and expectedLatestVersion"); + } + // The owner-scoped lookup fails closed for a cross-owner or cross-company id. + const current = await getUserSecretValueById(companyId, ownerUserId, input.expectedSecretId); + if (current.userSecretDefinitionId !== definition.id) { + throw notFound("User secret value not found"); + } + if (current.latestVersion !== input.expectedLatestVersion) { + throw conflict(CLAUDE_OAUTH_STALE_CONFIRMATION); + } + let rotated: CompanySecretRow; + try { + rotated = await secretService(db).rotate( + current.id, + { value, expectedLatestVersion: input.expectedLatestVersion }, + actor, + ); + } catch (error) { + if (error instanceof HttpError && error.status === 409) { + // A concurrent rotation bumped the version between the read and the + // predicate. Return the same fixed stale-confirmation conflict. + throw conflict(CLAUDE_OAUTH_STALE_CONFIRMATION); + } + throw error; + } + const stamped = await stampClaudeOAuthSessionId(rotated, sessionId); + return toClaudeOAuthResult(stamped); + } + + /** + * Reads the stored Claude Code OAuth value metadata for one owner. It returns + * only the secret id and the latest version, never the token. The client uses + * the version as the expected version of a later confirmed rotation. + * + * The reader derives no definition, no owner, and no secret id from a caller. + * It reads the fixed definition by the company and the fixed key. It reads the + * value by the company, the owner, and that definition. It returns null when no + * definition or no owner value exists, so a foreign value and a missing value + * look the same to the caller. It never creates the definition. + */ + async function readClaudeOAuthUserSecretStatusInternal( + companyId: string, + ownerUserId: string, + ): Promise<{ secretId: string; latestVersion: number } | null> { + const definition = await getUserSecretDefinitionByKey(companyId, CLAUDE_CODE_OAUTH_DEFINITION.key); + if (!definition || !isCompatibleClaudeOAuthDefinition(definition)) return null; + const existing = await getUserSecretValue({ companyId, ownerUserId, definitionId: definition.id }); + if (!existing) return null; + return { secretId: existing.id, latestVersion: existing.latestVersion }; + } + async function removeSecretInternal(secretId: string) { const secret = await getById(secretId); if (!secret) return null; @@ -2821,6 +3293,25 @@ export function secretService(db: Db) { createCurrentUserSecretValue: createUserSecretValueInternal, + // The narrow Claude Code OAuth definition helper. A caller passes + // no key, name, provider, mode, or status. The route calls it only after the + // authenticated board-user, company, and sandbox checks pass. + ensureClaudeOAuthUserSecretDefinition: ( + companyId: string, + actor?: { userId?: string | null; agentId?: string | null }, + ) => ensureClaudeOAuthUserSecretDefinitionInternal(companyId, actor), + + // The owner-bound Claude Code OAuth compare-and-set. It creates a + // first value or rotates after a confirmed expected version. The session id + // is the idempotency key for one completion. + completeClaudeOAuthUserSecret: completeClaudeOAuthUserSecretInternal, + + // The owner-bound Claude Code OAuth status read. It returns only + // the secret id and the latest version for the owner value, or null. It never + // returns the token and never creates the definition. The status route reads + // the expected version from it before it captures the confirmed rotation. + readClaudeOAuthUserSecretStatus: readClaudeOAuthUserSecretStatusInternal, + rotateCurrentUserSecretValue: async ( companyId: string, ownerUserId: string, @@ -2916,6 +3407,12 @@ export function secretService(db: Db) { companyId, { definitionKey: ref.definitionKey }, targetDb, + { + envKey: ref.envKey, + configPath: ref.configPath, + consumerType: target.targetType, + consumerId: target.targetId, + }, ); normalizedRefs.push({ definitionId: definition.id, @@ -2989,7 +3486,11 @@ export function secretService(db: Db) { const optionalBinding = input.allowMissingOverride || input.required === false; let definition: typeof userSecretDefinitions.$inferSelect; try { - definition = await resolveUserSecretDefinition(companyId, input); + definition = await resolveUserSecretDefinition(companyId, input, db, { + configPath: context?.configPath ?? null, + consumerType: context?.consumerType ?? null, + consumerId: context?.consumerId ?? null, + }); } catch (error) { if (optionalBinding && error instanceof HttpError && error.status === 404) return null; throw error; @@ -3370,6 +3871,7 @@ export function secretService(db: Db) { resolveSecretValueForAgentAccess, listAgentSecretAccess, resolveSecretValueForEphemeralAccess, + resolveSecretValueForSandboxCleanup, create: async ( companyId: string, @@ -3591,12 +4093,19 @@ export function secretService(db: Db) { externalRef?: string | null; providerVersionRef?: string | null; providerConfigId?: string | null; + // The optional owner-bound compare-and-set guard. When set, the final + // update matches the latest version, so a concurrent rotation between the + // read and the write cannot pass. A mismatch throws a 409 conflict. + expectedLatestVersion?: number; }, actor?: { userId?: string | null; agentId?: string | null }, ) => { const secret = await getById(secretId); if (!secret) throw notFound("Secret not found"); if (secret.status !== "active") throw unprocessable("Cannot rotate a non-active secret"); + if (input.expectedLatestVersion !== undefined && secret.latestVersion !== input.expectedLatestVersion) { + throw conflict(SECRET_VERSION_STALE_CONFLICT); + } const providerId = secret.provider as SecretProvider; const provider = getSecretProvider(providerId); const providerConfigId = @@ -3698,6 +4207,17 @@ export function secretService(db: Db) { operation: "rotate.prepare_rollback", }); } + // A guarded concurrent rotation inserted the same next version first, so + // this insert fails the (secretId, version) unique index. The loser never + // reaches the compare-and-set guard below. Normalize only that one + // collision to the same stale conflict, so a guarded rotation always + // returns one fixed 409. Re-throw every other error unchanged. + if ( + input.expectedLatestVersion !== undefined && + isUniqueConstraintViolation(error, COMPANY_SECRET_VERSION_UNIQUE_CONSTRAINT) + ) { + throw conflict(SECRET_VERSION_STALE_CONFLICT); + } throw error; } @@ -3727,11 +4247,25 @@ export function secretService(db: Db) { lastRotatedAt: new Date(), updatedAt: new Date(), }) - .where(eq(companySecrets.id, secret.id)) + .where(and( + eq(companySecrets.id, secret.id), + // The compare-and-set guard. It matches the latest version, so a + // concurrent rotation that already bumped it fails this predicate. + input.expectedLatestVersion === undefined + ? undefined + : eq(companySecrets.latestVersion, input.expectedLatestVersion), + )) .returning() .then((rows) => rows[0] ?? null); - if (!updated) throw notFound("Secret not found"); + if (!updated) { + // The predicate matched no row. A supplied expected version means a + // concurrent rotation won the race; return the stale conflict. An + // unguarded rotation means the secret is gone. + throw input.expectedLatestVersion === undefined + ? notFound("Secret not found") + : conflict(SECRET_VERSION_STALE_CONFLICT); + } return updated; }); } catch (error) { @@ -4209,7 +4743,12 @@ export function secretService(db: Db) { if (!parsed.success) continue; const binding = canonicalizeBinding(parsed.data as EnvBinding); if (binding.type === "user_secret_ref") { - await resolveUserSecretDefinition(companyId, { definitionKey: binding.key }, bindingDb); + await resolveUserSecretDefinition(companyId, { definitionKey: binding.key }, bindingDb, { + envKey: key, + configPath: `${pathPrefix}.${key}`, + consumerType: target.targetType, + consumerId: target.targetId, + }); userRefs.push({ definitionKey: binding.key, configPath: `${pathPrefix}.${key}`, @@ -4279,7 +4818,17 @@ export function secretService(db: Db) { if (userRefs.length === 0) return; const definitions = new Map(); for (const ref of userRefs) { - const definition = await resolveUserSecretDefinition(companyId, { definitionKey: ref.definitionKey }, targetDb); + const definition = await resolveUserSecretDefinition( + companyId, + { definitionKey: ref.definitionKey }, + targetDb, + { + envKey: ref.envKey, + configPath: ref.configPath, + consumerType: target.targetType, + consumerId: target.targetId, + }, + ); definitions.set(ref.definitionKey, definition.id); } await targetDb.insert(userSecretDeclarations).values( diff --git a/server/src/services/setup-token-session.test.ts b/server/src/services/setup-token-session.test.ts index 7614d9a807..10d9fac3c0 100644 --- a/server/src/services/setup-token-session.test.ts +++ b/server/src/services/setup-token-session.test.ts @@ -1,5 +1,16 @@ -import { describe, expect, it } from "vitest"; +import { randomUUID } from "node:crypto"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { eq, sql } from "drizzle-orm"; import { + claudeSetupTokenSessions, + companies, + createDb, + environments, + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "@paperclipai/db"; +import { + createDbSetupTokenCleanupStore, SetupTokenSessionService, SetupTokenSessionError, assessConfidentialStartup, @@ -11,6 +22,8 @@ import { SETUP_TOKEN_RATE_LIMITED, SETUP_TOKEN_CAP_EXCEEDED, SETUP_TOKEN_TOKEN_UNAVAILABLE, + SETUP_TOKEN_STORAGE_FAILED, + type SetupTokenCleanupIdentity, type SetupTokenCleanupRecord, type SetupTokenCleanupStore, type SetupTokenCredentialSink, @@ -21,6 +34,7 @@ import { type SetupTokenLoginProcessFactory, type SetupTokenPromptSink, type SetupTokenRateLimiter, + type SetupTokenSecretWriter, type SetupTokenSessionScope, } from "./setup-token-session.js"; import { redactSensitive } from "../middleware/redact-sensitive.js"; @@ -37,6 +51,8 @@ const OWNER_SCOPE: SetupTokenSessionScope = { companyId: "company-1", ownerUserId: "user-1", targetAgentId: "agent-1", + adapterType: "claude_local", + environmentId: "env-1", }; /** A controllable fake login process. It records the call order into `events`. */ @@ -59,8 +75,10 @@ class FakeProcess implements SetupTokenLoginProcess { surfacePrompt(url: string): void { this.onPrompt({ url }); } - surfaceCredential(token: string): void { - this.onCredential(token); + // The credential sink is asynchronous: it awaits the owner-bound secret write. + // The test awaits this call so the write settles before it asserts. + async surfaceCredential(token: string): Promise { + await this.onCredential(token); } finish(outcome: SetupTokenLoginOutcome): void { this.resolveDone(outcome); @@ -103,61 +121,168 @@ class FakeLeaseManager implements SetupTokenLeaseManager { } } +// A lease manager whose `acquire` stays in flight until the test resolves it. +// The test uses it to hold the first start suspended on the lease acquire, then +// prove a concurrent start for the same owner fails closed before it reaches the +// provider. It can also fail the next acquire, to drive the acquire-failure +// rollback path. +class DeferredLeaseManager implements SetupTokenLeaseManager { + acquireCalls = 0; + released: string[] = []; + releaseByIdCalls: string[] = []; + failNextAcquire = false; + private counter = 0; + private readonly pending: Array<{ resolve: (lease: SetupTokenLease) => void; id: string }> = []; + async acquire(): Promise { + this.acquireCalls += 1; + if (this.failNextAcquire) { + this.failNextAcquire = false; + throw new Error("lease acquire failed"); + } + const id = `lease-${(this.counter += 1)}`; + return new Promise((resolve) => { + this.pending.push({ resolve, id }); + }); + } + /** Resolves the oldest in-flight acquire with its lease. */ + resolveNextAcquire(): void { + const next = this.pending.shift(); + if (next) next.resolve({ id: next.id }); + } + /** The count of acquires that are in flight. */ + pendingCount(): number { + return this.pending.length; + } + async release(lease: SetupTokenLease): Promise { + this.released.push(lease.id); + } + async releaseById(leaseId: string): Promise { + this.releaseByIdCalls.push(leaseId); + this.released.push(leaseId); + } +} + +// Builds the durable-record identity from a session scope and the session id. The +// fake writers use it to simulate the atomic durable `stored` transition that the +// production writer performs inside the credential transaction. +function identityFromScope(scope: SetupTokenSessionScope, sessionId: string): SetupTokenCleanupIdentity { + return { + sessionId, + companyId: scope.companyId, + ownerUserId: scope.ownerUserId, + adapterType: scope.adapterType, + environmentId: scope.environmentId, + }; +} + +function identityMatchesRow(row: SetupTokenCleanupRecord, identity: SetupTokenCleanupIdentity): boolean { + return ( + row.companyId === identity.companyId && + row.ownerUserId === identity.ownerUserId && + row.adapterType === identity.adapterType && + row.environmentId === identity.environmentId + ); +} + class FakeStore implements SetupTokenCleanupStore { rows = new Map(); async record(record: SetupTokenCleanupRecord): Promise { this.rows.set(record.sessionId, { ...record }); } - async markState(sessionId: string, state: SetupTokenCleanupRecord["state"]): Promise { - const row = this.rows.get(sessionId); - if (row) row.state = state; + async markState(identity: SetupTokenCleanupIdentity, state: SetupTokenCleanupRecord["state"]): Promise { + const row = this.rows.get(identity.sessionId); + // The write matches the full owner scope, so it never updates a row by the + // session id alone. + if (row && identityMatchesRow(row, identity)) row.state = state; } - async remove(sessionId: string): Promise { - this.rows.delete(sessionId); + async remove(identity: SetupTokenCleanupIdentity): Promise { + // The delete matches the full owner scope, so it never removes a row by the + // session id alone. + const row = this.rows.get(identity.sessionId); + if (row && identityMatchesRow(row, identity)) this.rows.delete(identity.sessionId); } async listReapable(now: number): Promise { return [...this.rows.values()].filter( - (row) => isTerminalSessionState(row.state) || row.deadline <= now, + (row) => isTerminalSessionState(row.state) || row.deadline <= now || row.boundAt !== null, ); } + async consumeStoredClaim(identity: SetupTokenCleanupIdentity): Promise { + const row = this.rows.get(identity.sessionId); + if ( + !row || + !identityMatchesRow(row, identity) || + row.state !== "stored" || + row.boundAt !== null || + row.deadline <= Date.now() + ) { + return null; + } + row.boundAt = Date.now(); + return { ...row }; + } } function allowAllRateLimiter(): SetupTokenRateLimiter { return { consume: () => ({ allowed: true, retryAfterSeconds: 0 }) }; } -function buildService(overrides: { +// One recorded owner-bound secret write. The fake writer records only the +// non-secret ids and the token, so a test can prove the token reached the writer +// and never the store or a log. +interface RecordedSecretWrite { + scope: SetupTokenSessionScope; + sessionId: string; + token: string; +} + +function buildService(overrides: { events?: string[]; - leases?: FakeLeaseManager; + leases?: L; store?: FakeStore; rateLimiter?: SetupTokenRateLimiter; caps?: { perOwner: number; perAgent: number; perCompany: number }; ttlMs?: number; tokenRetentionMs?: number; now?: () => number; + completeCredential?: SetupTokenSecretWriter; + factory?: SetupTokenLoginProcessFactory; } = {}) { const events = overrides.events ?? []; const processes: FakeProcess[] = []; let processCounter = 0; - const factory: SetupTokenLoginProcessFactory = ({ onPrompt, onCredential }) => { - processCounter += 1; - const process = new FakeProcess(`p${processCounter}`, onPrompt, onCredential, events); - processes.push(process); - return process; - }; - const leases = overrides.leases ?? new FakeLeaseManager(events); + const factory: SetupTokenLoginProcessFactory = + overrides.factory ?? + (({ onPrompt, onCredential }) => { + processCounter += 1; + const process = new FakeProcess(`p${processCounter}`, onPrompt, onCredential, events); + processes.push(process); + return process; + }); + const leases: L = overrides.leases ?? (new FakeLeaseManager(events) as unknown as L); const store = overrides.store ?? new FakeStore(); + const secretWrites: RecordedSecretWrite[] = []; + const completeCredential: SetupTokenSecretWriter = + overrides.completeCredential ?? + (async (input) => { + secretWrites.push({ scope: input.scope, sessionId: input.sessionId, token: input.token }); + // Simulate the atomic durable `stored` transition the production writer runs + // inside the credential transaction, so the store reflects the claim. + await store.markState(identityFromScope(input.scope, input.sessionId), "stored"); + }); + const logs: string[] = []; const service = new SetupTokenSessionService({ factory, leases, store, + completeCredential, rateLimiter: overrides.rateLimiter ?? allowAllRateLimiter(), caps: overrides.caps ?? { perOwner: 5, perAgent: 5, perCompany: 5 }, ttlMs: overrides.ttlMs ?? 60_000, tokenRetentionMs: overrides.tokenRetentionMs, now: overrides.now, + log: (line) => logs.push(line), }); - return { service, processes, leases, store, events }; + return { service, processes, leases, store, events, secretWrites, logs }; } describe("SetupTokenSessionService.start", () => { @@ -206,6 +331,140 @@ describe("SetupTokenSessionService.start", () => { }); }); +describe("SetupTokenSessionService.start cap reservation (concurrency)", () => { + const capsPerOwnerOne = { perOwner: 1, perAgent: 5, perCompany: 5 }; + + it("reserves the owner slot synchronously, so a concurrent start fails closed with 429", async () => { + const leases = new DeferredLeaseManager(); + const { service } = buildService({ leases, caps: capsPerOwnerOne }); + + // Start the first session. It reserves the owner slot synchronously, then it + // suspends on the deferred lease acquire. The acquire reached the provider and + // has not resolved yet. + const firstStart = service.start(OWNER_SCOPE); + await flush(); + expect(leases.acquireCalls).toBe(1); + expect(leases.pendingCount()).toBe(1); + + // The second start for the same owner runs while the first acquire is in + // flight. The synchronous reservation already holds the one owner slot, so the + // second start fails closed with the fixed 429 cap error before it reaches the + // provider. Only one acquire ever reaches the provider. + await expect(service.start(OWNER_SCOPE)).rejects.toMatchObject({ + status: 429, + message: SETUP_TOKEN_CAP_EXCEEDED, + }); + expect(leases.acquireCalls).toBe(1); + + // Let the first start finish. It holds the one live session. + leases.resolveNextAcquire(); + const first = await firstStart; + expect(first.sessionId).toBeTruthy(); + expect(service.activeSessionCount()).toBe(1); + }); + + it("releases the owner reservation after cleanup, so the owner can start again", async () => { + const leases = new DeferredLeaseManager(); + const { service } = buildService({ leases, caps: capsPerOwnerOne }); + + const firstStart = service.start(OWNER_SCOPE); + await flush(); + leases.resolveNextAcquire(); + const first = await firstStart; + + // Cancel the live session. The cleanup releases the owner reservation. + await service.cancel(first.sessionId, OWNER_SCOPE); + expect(service.activeSessionCount()).toBe(0); + + // A new start for the same owner reserves the slot again and starts, so the + // cleanup released the reservation exactly once and left no slot held. + const secondStart = service.start(OWNER_SCOPE); + await flush(); + leases.resolveNextAcquire(); + const second = await secondStart; + expect(second.sessionId).toBeTruthy(); + expect(second.sessionId).not.toBe(first.sessionId); + expect(service.activeSessionCount()).toBe(1); + }); + + it("releases the owner reservation when the lease acquire fails, so a retry can start", async () => { + const leases = new DeferredLeaseManager(); + leases.failNextAcquire = true; + const { service } = buildService({ leases, caps: capsPerOwnerOne }); + + // The first start reserves the owner slot, then the lease acquire rejects. The + // failure path rolls back the reservation and holds no durable state. + await expect(service.start(OWNER_SCOPE)).rejects.toThrow("lease acquire failed"); + expect(service.activeSessionCount()).toBe(0); + expect(leases.released).toEqual([]); + + // A retry for the same owner reserves the slot again and starts. The failed + // start left no reservation behind. + const retry = service.start(OWNER_SCOPE); + await flush(); + expect(leases.acquireCalls).toBe(2); + leases.resolveNextAcquire(); + const result = await retry; + expect(result.sessionId).toBeTruthy(); + expect(service.activeSessionCount()).toBe(1); + }); + + it("releases the reservation and the lease when the durable record write fails", async () => { + const leases = new FakeLeaseManager(); + const store = new FakeStore(); + let failRecord = true; + const recordOriginal = store.record.bind(store); + store.record = async (record) => { + if (failRecord) { + failRecord = false; + throw new Error("durable write failed"); + } + return recordOriginal(record); + }; + const { service } = buildService({ leases, store, caps: capsPerOwnerOne }); + + // The durable write fails. The failure path releases the acquired lease and + // rolls back the reservation. + await expect(service.start(OWNER_SCOPE)).rejects.toThrow("durable write failed"); + expect(leases.released).toEqual(["lease-1"]); + expect(service.activeSessionCount()).toBe(0); + + // The reservation rolled back, so a retry for the same owner starts. + const retry = await service.start(OWNER_SCOPE); + expect(retry.sessionId).toBeTruthy(); + expect(service.activeSessionCount()).toBe(1); + }); + + it("releases the reservation and the lease when the factory throws, so a retry can start", async () => { + const leases = new FakeLeaseManager(); + let failFactory = true; + const factory: SetupTokenLoginProcessFactory = () => { + if (failFactory) { + failFactory = false; + throw new Error("factory failed"); + } + // A minimal live process for the retry. It never ends on its own. + return { + done: new Promise(() => {}), + submitCode: () => {}, + stop: () => {}, + }; + }; + const { service } = buildService({ leases, factory, caps: capsPerOwnerOne }); + + // The factory throws. The start releases the lease, drops the durable record, + // rolls back the reservation, and returns the fixed 503 start error. + await expect(service.start(OWNER_SCOPE)).rejects.toMatchObject({ status: 503 }); + expect(leases.released).toEqual(["lease-1"]); + expect(service.activeSessionCount()).toBe(0); + + // The reservation rolled back, so a retry for the same owner starts. + const retry = await service.start(OWNER_SCOPE); + expect(retry.sessionId).toBeTruthy(); + expect(service.activeSessionCount()).toBe(1); + }); +}); + describe("SetupTokenSessionService.readPrompt", () => { it("returns the full login URL to the authorized owner once the prompt surfaces", async () => { const { service, processes } = buildService(); @@ -248,16 +507,26 @@ describe("SetupTokenSessionService authorization boundary", () => { const crossCompany: SetupTokenSessionScope = { ...OWNER_SCOPE, companyId: "company-2" }; const sameCompanyOtherUser: SetupTokenSessionScope = { ...OWNER_SCOPE, ownerUserId: "user-9" }; const otherAgent: SetupTokenSessionScope = { ...OWNER_SCOPE, targetAgentId: "agent-9" }; + const otherAdapter: SetupTokenSessionScope = { ...OWNER_SCOPE, adapterType: "codex_local" }; + const otherEnvironment: SetupTokenSessionScope = { ...OWNER_SCOPE, environmentId: "env-9" }; it("returns the same not-found for a cross-scope caller on every operation", async () => { const { service, processes } = buildService(); const { sessionId } = await service.start(OWNER_SCOPE); processes[0].surfacePrompt(FULL_LOGIN_URL); - for (const scope of [crossCompany, sameCompanyOtherUser, otherAgent]) { + // The full-scope match rejects a mismatch in any of the five scope fields: + // the company, the owner, the agent, the adapter, and the environment. + for (const scope of [ + crossCompany, + sameCompanyOtherUser, + otherAgent, + otherAdapter, + otherEnvironment, + ]) { expect(() => service.readPrompt(sessionId, scope)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); expect(() => service.submitCode(sessionId, scope, "code")).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); - expect(() => service.receiveToken(sessionId, scope)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); + expect(() => service.completeSession(sessionId, scope)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); await expect(service.cancel(sessionId, scope)).rejects.toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); await expect(service.expire(sessionId, scope)).rejects.toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); } @@ -269,6 +538,57 @@ describe("SetupTokenSessionService authorization boundary", () => { }); }); +describe("SetupTokenSessionService company-and-environment scope", () => { + // The agentless company scope. It carries a null agent id. + const COMPANY_SCOPE: SetupTokenSessionScope = { ...OWNER_SCOPE, targetAgentId: null }; + const companyKey = { + companyId: COMPANY_SCOPE.companyId, + ownerUserId: COMPANY_SCOPE.ownerUserId, + adapterType: COMPANY_SCOPE.adapterType, + }; + + it("resolves an agentless session by the company key and returns the intrinsic environment", async () => { + const { service, processes } = buildService(); + const { sessionId } = await service.start(COMPANY_SCOPE); + processes[0].surfacePrompt(FULL_LOGIN_URL); + + const scope = service.resolveCompanyScope(sessionId, companyKey); + expect(scope.targetAgentId).toBeNull(); + expect(scope.environmentId).toBe(COMPANY_SCOPE.environmentId); + + const descriptor = service.describeOwned(sessionId, scope); + expect(descriptor.sessionId).toBe(sessionId); + expect(descriptor.environmentId).toBe(COMPANY_SCOPE.environmentId); + expect(descriptor.loginUrl).toBe(FULL_LOGIN_URL); + }); + + it("rejects an agent-scoped session through the company key", async () => { + const { service } = buildService(); + // The session carries a non-null agent id. The company key requires the + // agentless marker, so a company route cannot reach an agent session. + const { sessionId } = await service.start(OWNER_SCOPE); + expect(() => service.resolveCompanyScope(sessionId, companyKey)).toThrow( + SETUP_TOKEN_SESSION_NOT_FOUND, + ); + }); + + it("returns the same not-found for a foreign company key", async () => { + const { service } = buildService(); + const { sessionId } = await service.start(COMPANY_SCOPE); + // A cross-company, a cross-owner, and a cross-adapter key each return the + // same not-found error as a missing session. + for (const key of [ + { ...companyKey, companyId: "company-2" }, + { ...companyKey, ownerUserId: "user-9" }, + { ...companyKey, adapterType: "codex_local" }, + ]) { + expect(() => service.resolveCompanyScope(sessionId, key)).toThrow( + SETUP_TOKEN_SESSION_NOT_FOUND, + ); + } + }); +}); + describe("SetupTokenSessionService cleanup order", () => { it("stops the direct child before the lease release on cancel", async () => { const events: string[] = []; @@ -313,10 +633,12 @@ describe("SetupTokenSessionService durable reaper", () => { sessionId: "orphan-1", companyId: "company-1", ownerUserId: "user-1", - targetAgentId: "agent-1", + adapterType: "claude_local", + environmentId: "env-1", leaseId: "lease-orphan", deadline: 1_000, state: "awaiting_code", + boundAt: null, }); const { service } = buildService({ store, leases, now: () => 5_000 }); const summary = await service.reap(5_000); @@ -331,10 +653,12 @@ describe("SetupTokenSessionService durable reaper", () => { sessionId: "orphan-2", companyId: "company-1", ownerUserId: "user-1", - targetAgentId: "agent-1", + adapterType: "claude_local", + environmentId: "env-1", leaseId: "lease-orphan-2", deadline: 1_000, state: "failed", + boundAt: null, }); const leases = new FakeLeaseManager(); leases.releaseById = async () => { @@ -347,43 +671,228 @@ describe("SetupTokenSessionService durable reaper", () => { }); }); -describe("SetupTokenSessionService.receiveToken", () => { - it("returns the token once to the authorized owner after a successful login", async () => { - const { service, processes, leases } = buildService(); +describe("SetupTokenSessionService.completeSession", () => { + it("returns the non-secret storedSessionId after a successful secret write, and no token", async () => { + const { service, processes, leases, store, secretWrites, logs } = buildService(); const { sessionId } = await service.start(OWNER_SCOPE); processes[0].surfacePrompt(FULL_LOGIN_URL); service.submitCode(sessionId, OWNER_SCOPE, "code"); - processes[0].surfaceCredential(SYNTH_TOKEN); + // The session awaits the owner-bound secret write before it completes. + await processes[0].surfaceCredential(SYNTH_TOKEN); processes[0].finish("success"); await new Promise((resolve) => setImmediate(resolve)); - // The service releases the sandbox lease at once, but it retains the token - // for the owner to receive it one time. + + // The token reached the owner-bound writer, once, with the owner scope. + expect(secretWrites).toEqual([{ scope: OWNER_SCOPE, sessionId, token: SYNTH_TOKEN }]); + // The service releases the sandbox lease at once. expect(leases.released).toEqual(["lease-1"]); - const received = service.receiveToken(sessionId, OWNER_SCOPE); - expect(received.token).toBe(SYNTH_TOKEN); - // One-shot: a second receive returns the same not-found as a missing session. - expect(() => service.receiveToken(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); + // The durable row stays as the stored-session claim, not removed. + expect(store.rows.get(sessionId)?.state).toBe("stored"); + + // The completion returns the non-secret storedSessionId and carries no token. + const completion = service.completeSession(sessionId, OWNER_SCOPE); + expect(completion.storedSessionId).toBe(sessionId); + expect(completion).not.toHaveProperty("token"); + + // No response, log, or durable record contains the token after success. + const haystack = `${logs.join("\n")}\n${JSON.stringify(completion)}\n${JSON.stringify([ + ...store.rows.values(), + ])}`; + expect(haystack).not.toContain(SYNTH_TOKEN); }); - it("returns the fixed unavailable error before the token is delivered", async () => { + it("returns the fixed unavailable error before the secret write completes", async () => { const { service, processes } = buildService(); const { sessionId } = await service.start(OWNER_SCOPE); processes[0].surfacePrompt(FULL_LOGIN_URL); service.submitCode(sessionId, OWNER_SCOPE, "code"); - // The login has not delivered the token yet, so receive-token is unavailable. - expect(() => service.receiveToken(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_TOKEN_UNAVAILABLE); + // The secret write has not run, so the completion is unavailable. + expect(() => service.completeSession(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_TOKEN_UNAVAILABLE); }); - it("purges the retained token when the retention window ends", async () => { + it("reaches failed with no binding and no completion when the secret write errors", async () => { + const store = new FakeStore(); + const completeCredential: SetupTokenSecretWriter = async () => { + throw new Error("storage down"); + }; + const { service, processes, leases, logs } = buildService({ store, completeCredential }); + const { sessionId } = await service.start(OWNER_SCOPE); + processes[0].surfacePrompt(FULL_LOGIN_URL); + service.submitCode(sessionId, OWNER_SCOPE, "code"); + + // The sink rejects with the fixed, non-secret storage error. It does not + // swallow the failure. + await expect(processes[0].surfaceCredential(SYNTH_TOKEN)).rejects.toMatchObject({ + message: SETUP_TOKEN_STORAGE_FAILED, + }); + // Let the terminal-state handler run. + processes[0].finish("failure"); + await new Promise((resolve) => setImmediate(resolve)); + + // The session failed: it holds no live session and released the lease. + expect(service.activeSessionCount()).toBe(0); + expect(leases.released).toEqual(["lease-1"]); + // No stored-session claim exists: the durable row never reached stored. + expect(store.rows.has(sessionId)).toBe(false); + // The completion is gone, so a read returns the same not-found error. + expect(() => service.completeSession(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); + // No log line contains the token after the failure. + expect(logs.join("\n")).not.toContain(SYNTH_TOKEN); + }); + + it("purges the retained completion when the retention window ends", async () => { const { service, processes } = buildService({ tokenRetentionMs: 5 }); const { sessionId } = await service.start(OWNER_SCOPE); processes[0].surfacePrompt(FULL_LOGIN_URL); service.submitCode(sessionId, OWNER_SCOPE, "code"); - processes[0].surfaceCredential(SYNTH_TOKEN); + await processes[0].surfaceCredential(SYNTH_TOKEN); processes[0].finish("success"); await new Promise((resolve) => setTimeout(resolve, 20)); - // The retention timer purged the token, so the session is gone. - expect(() => service.receiveToken(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); + // The retention timer dropped the in-memory session, so a read is not found. + expect(() => service.completeSession(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); + }); +}); + +// A deferred owner-bound secret writer. The test controls when the write +// settles, so it can hold the write in flight and drive a cancel or an expiry +// into the exact window that used to race the write. It records only the +// non-secret ids and the token, and it counts how many times the writer started. +function buildDeferredWriter(store: FakeStore) { + const calls: RecordedSecretWrite[] = []; + let started = 0; + let pendingInput: { scope: SetupTokenSessionScope; sessionId: string } | null = null; + let resolveWrite: (() => void) | null = null; + let rejectWrite: ((error: Error) => void) | null = null; + const writer: SetupTokenSecretWriter = (input) => { + started += 1; + pendingInput = { scope: input.scope, sessionId: input.sessionId }; + calls.push({ scope: input.scope, sessionId: input.sessionId, token: input.token }); + return new Promise((resolve, reject) => { + resolveWrite = resolve; + rejectWrite = reject; + }); + }; + return { + writer, + calls, + startedCount: () => started, + // The commit resolves the write. It first simulates the atomic durable + // `stored` transition the production writer runs inside the same transaction. + resolve: async () => { + if (pendingInput) { + await store.markState(identityFromScope(pendingInput.scope, pendingInput.sessionId), "stored"); + } + resolveWrite?.(); + }, + reject: () => rejectWrite?.(new Error("storage down")), + }; +} + +// Yields to the microtask queue, so a scheduled sink or lock step can run. +const flush = () => new Promise((resolve) => setImmediate(resolve)); + +describe("SetupTokenSessionService terminal-race credential write", () => { + for (const variant of [ + { label: "cancel", run: (service: SetupTokenSessionService, sessionId: string) => service.cancel(sessionId, OWNER_SCOPE), terminalState: "cancelled" as const }, + { label: "expiry", run: (service: SetupTokenSessionService, sessionId: string) => service.expire(sessionId, OWNER_SCOPE), terminalState: "timed_out" as const }, + ]) { + it(`writes no secret when a ${variant.label} wins the race, and leaves no stored claim`, async () => { + const store = new FakeStore(); + const deferred = buildDeferredWriter(store); + const { service, processes, leases } = buildService({ store, completeCredential: deferred.writer }); + const { sessionId } = await service.start(OWNER_SCOPE); + processes[0].surfacePrompt(FULL_LOGIN_URL); + service.submitCode(sessionId, OWNER_SCOPE, "code"); + + // The terminal transition wins: it reaches the terminal state before the + // credential arrives. The cleanup removes the durable row. + await variant.run(service, sessionId); + expect(store.rows.has(sessionId)).toBe(false); + + // The credential arrives after the terminal transition. The sink fails + // closed: it never calls the writer and it rejects with the fixed, + // non-secret error. + await expect(processes[0].surfaceCredential(SYNTH_TOKEN)).rejects.toMatchObject({ + message: SETUP_TOKEN_STORAGE_FAILED, + }); + + // The writer never committed, so no unintended secret write happened. + expect(deferred.startedCount()).toBe(0); + expect(deferred.calls).toHaveLength(0); + // The durable row holds no stored claim. + expect(store.rows.has(sessionId)).toBe(false); + // The completion has no success: a read returns the same not-found error. + expect(() => service.completeSession(sessionId, OWNER_SCOPE)).toThrow(SETUP_TOKEN_SESSION_NOT_FOUND); + // The session released its lease and holds no live session. + expect(leases.released).toEqual(["lease-1"]); + expect(service.activeSessionCount()).toBe(0); + }); + + it(`does not erase the stored claim when the write wins the race, then a ${variant.label} arrives`, async () => { + const store = new FakeStore(); + const deferred = buildDeferredWriter(store); + const { service, processes, leases } = buildService({ store, completeCredential: deferred.writer }); + const { sessionId } = await service.start(OWNER_SCOPE); + processes[0].surfacePrompt(FULL_LOGIN_URL); + service.submitCode(sessionId, OWNER_SCOPE, "code"); + + // Start the credential write. It is in flight and holds the session lock, so + // a terminal transition that arrives now must wait for the write to settle. + const credentialSettled = processes[0].surfaceCredential(SYNTH_TOKEN); + await flush(); + expect(deferred.startedCount()).toBe(1); + + // The terminal transition arrives while the write is in flight. It blocks on + // the lock; it does not interleave with the write. + const terminalSettled = variant.run(service, sessionId); + await flush(); + + // The write wins the serialized ordering: it commits, then the service + // records the non-secret markers and releases the lock. + await deferred.resolve(); + await credentialSettled; + await terminalSettled; + + // The writer committed exactly once. No duplicate write is possible. + expect(deferred.startedCount()).toBe(1); + expect(deferred.calls).toEqual([{ scope: OWNER_SCOPE, sessionId, token: SYNTH_TOKEN }]); + // The terminal transition did not erase the claim: the durable row stays + // `stored` and the terminal API reports the completed state. + expect(store.rows.get(sessionId)?.state).toBe("stored"); + const completion = service.completeSession(sessionId, OWNER_SCOPE); + expect(completion.storedSessionId).toBe(sessionId); + // The service released the sandbox lease at once. + expect(leases.released).toEqual(["lease-1"]); + }); + } + + it("does not erase the claim when the process reports success after the write wins a cancel race", async () => { + // This proves the natural success path stays intact when a cancel loses the + // race: the write commits, the cancel completes the session, and the later + // process-done success transition is an idempotent no-op. + const store = new FakeStore(); + const deferred = buildDeferredWriter(store); + const { service, processes } = buildService({ store, completeCredential: deferred.writer }); + const { sessionId } = await service.start(OWNER_SCOPE); + processes[0].surfacePrompt(FULL_LOGIN_URL); + service.submitCode(sessionId, OWNER_SCOPE, "code"); + + const credentialSettled = processes[0].surfaceCredential(SYNTH_TOKEN); + await flush(); + const cancelSettled = service.cancel(sessionId, OWNER_SCOPE); + await flush(); + await deferred.resolve(); + await credentialSettled; + await cancelSettled; + + // The process reports success after the sink resolved. The transition finds a + // completed, cleaned-up session and does nothing. + processes[0].finish("success"); + await flush(); + + expect(deferred.startedCount()).toBe(1); + expect(store.rows.get(sessionId)?.state).toBe("stored"); + expect(service.completeSession(sessionId, OWNER_SCOPE).storedSessionId).toBe(sessionId); }); }); @@ -394,7 +903,7 @@ describe("no secret reaches a sink (SR-1, SR-5)", () => { const { sessionId } = await service.start(OWNER_SCOPE); processes[0].surfacePrompt(FULL_LOGIN_URL); service.submitCode(sessionId, OWNER_SCOPE, "SECRETCODE123"); - processes[0].surfaceCredential(SYNTH_TOKEN); + await processes[0].surfaceCredential(SYNTH_TOKEN); const serialized = JSON.stringify([...store.rows.values()]); expect(serialized).not.toContain("SECRETCODE123"); expect(serialized).not.toContain("STATEVALUE"); @@ -545,3 +1054,255 @@ describe("confidential transport guard (SR-6, SR-7)", () => { expect(decision.allowed).toBe(false); }); }); + +describe("SetupTokenSessionService durable state scope", () => { + it("marks the durable record state by the full owner scope", async () => { + const store = new FakeStore(); + const { service, processes } = buildService({ store }); + const { sessionId } = await service.start(OWNER_SCOPE); + processes[0].surfacePrompt(FULL_LOGIN_URL); + await new Promise((resolve) => setImmediate(resolve)); + expect(store.rows.get(sessionId)?.state).toBe("awaiting_code"); + + // A foreign-scope mark leaves the row unchanged, so a write never updates a + // row by the session id alone. + await store.markState( + { + sessionId, + companyId: OWNER_SCOPE.companyId, + ownerUserId: "intruder", + adapterType: OWNER_SCOPE.adapterType, + environmentId: OWNER_SCOPE.environmentId, + }, + "submitting", + ); + expect(store.rows.get(sessionId)?.state).toBe("awaiting_code"); + }); +}); + +// --- The durable, database-backed cleanup store ------------------------------ + +const delay = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +if (!embeddedPostgresSupport.supported) { + console.warn( + `Skipping durable setup-token cleanup store tests on this host: ${ + embeddedPostgresSupport.reason ?? "unsupported environment" + }`, + ); +} + +describeEmbeddedPostgres("durable setup-token cleanup store (embedded postgres)", () => { + let stopDb: (() => Promise) | undefined; + let connectionString!: string; + let db!: ReturnType; + + beforeAll(async () => { + const started = await startEmbeddedPostgresTestDatabase("claude-setup-token-store"); + stopDb = started.cleanup; + connectionString = started.connectionString; + db = createDb(connectionString); + }); + + afterEach(async () => { + await db.delete(claudeSetupTokenSessions); + await db.delete(environments); + await db.delete(companies); + }); + + afterAll(async () => { + await stopDb?.(); + }); + + // Seed a company and an environment, then return one fresh record identity. + async function seedScope(): Promise { + const companyId = randomUUID(); + const environmentId = randomUUID(); + await db.insert(companies).values({ + id: companyId, + name: "Acme", + status: "active", + // A unique issue prefix per company; the column has a unique index. + issuePrefix: companyId.slice(0, 8), + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(environments).values({ + id: environmentId, + name: `sandbox-${environmentId.slice(0, 8)}`, + driver: "sandbox", + status: "active", + config: { provider: "fake" }, + createdAt: new Date(), + updatedAt: new Date(), + }); + return { + sessionId: randomUUID(), + companyId, + ownerUserId: `user-${randomUUID().slice(0, 8)}`, + adapterType: "claude_local", + environmentId, + }; + } + + async function insertRecord( + identity: SetupTokenCleanupIdentity, + state: SetupTokenCleanupRecord["state"], + deadlineMs: number, + ): Promise { + const store = createDbSetupTokenCleanupStore(db); + await store.record({ + ...identity, + leaseId: "lease-1", + deadline: deadlineMs, + state, + boundAt: null, + }); + } + + async function readRow(sessionId: string) { + const rows = await db + .select() + .from(claudeSetupTokenSessions) + .where(eq(claudeSetupTokenSessions.sessionId, sessionId)); + return rows[0]; + } + + it("records the non-secret cleanup record and reads it back", async () => { + const identity = await seedScope(); + await insertRecord(identity, "starting", Date.now() + 60_000); + const row = await readRow(identity.sessionId); + expect(row?.companyId).toBe(identity.companyId); + expect(row?.ownerUserId).toBe(identity.ownerUserId); + expect(row?.adapterType).toBe(identity.adapterType); + expect(row?.environmentId).toBe(identity.environmentId); + expect(row?.state).toBe("starting"); + expect(row?.boundAt).toBeNull(); + }); + + it("consumes a stored claim once with one conditional write", async () => { + const identity = await seedScope(); + await insertRecord(identity, "stored", Date.now() + 60_000); + const store = createDbSetupTokenCleanupStore(db); + + const first = await store.consumeStoredClaim(identity); + expect(first).not.toBeNull(); + expect(first?.sessionId).toBe(identity.sessionId); + expect(first?.boundAt).not.toBeNull(); + expect((await readRow(identity.sessionId))?.boundAt).not.toBeNull(); + + // A second consume finds the claim already consumed and returns no row. + const second = await store.consumeStoredClaim(identity); + expect(second).toBeNull(); + }); + + it("returns no row for a foreign-scope consume and leaves the claim unconsumed", async () => { + const identity = await seedScope(); + await insertRecord(identity, "stored", Date.now() + 60_000); + const store = createDbSetupTokenCleanupStore(db); + + const foreign = await store.consumeStoredClaim({ ...identity, ownerUserId: "intruder" }); + expect(foreign).toBeNull(); + expect((await readRow(identity.sessionId))?.boundAt).toBeNull(); + }); + + it("returns no row for an expired claim", async () => { + const identity = await seedScope(); + await insertRecord(identity, "stored", Date.now() - 1_000); + const store = createDbSetupTokenCleanupStore(db); + + const consumed = await store.consumeStoredClaim(identity); + expect(consumed).toBeNull(); + expect((await readRow(identity.sessionId))?.boundAt).toBeNull(); + }); + + it("returns no row for a non-stored claim", async () => { + const identity = await seedScope(); + await insertRecord(identity, "submitting", Date.now() + 60_000); + const store = createDbSetupTokenCleanupStore(db); + + const consumed = await store.consumeStoredClaim(identity); + expect(consumed).toBeNull(); + expect((await readRow(identity.sessionId))?.boundAt).toBeNull(); + }); + + it("lists terminal, expired, and consumed records but not a live stored claim", async () => { + const store = createDbSetupTokenCleanupStore(db); + const now = Date.now(); + + const terminal = await seedScope(); + await insertRecord(terminal, "failed", now + 60_000); + + const expired = await seedScope(); + await insertRecord(expired, "awaiting_code", now - 1_000); + + const consumed = await seedScope(); + await insertRecord(consumed, "stored", now + 60_000); + await store.consumeStoredClaim(consumed); + + const liveStored = await seedScope(); + await insertRecord(liveStored, "stored", now + 60_000); + + const reapable = await store.listReapable(now); + const ids = new Set(reapable.map((record) => record.sessionId)); + expect(ids.has(terminal.sessionId)).toBe(true); + expect(ids.has(expired.sessionId)).toBe(true); + expect(ids.has(consumed.sessionId)).toBe(true); + // A live, unexpired, unconsumed stored claim is not reapable. + expect(ids.has(liveStored.sessionId)).toBe(false); + }); + + it("returns no row when the claim row lock holds until after the deadline", async () => { + // This test proves the consume uses `clock_timestamp()`, not + // `transaction_timestamp()`. The row starts with a far-future deadline, so + // the consume treats it as a valid candidate and waits for the row lock. A + // second connection holds the lock, sets a near deadline inside the locked + // transaction, and commits after that deadline passes. The consume then + // re-checks the predicate against the committed row with the current + // database time, so the expired claim returns no row. A + // `transaction_timestamp()` predicate would use the stale start time and + // wrongly consume the claim. + const identity = await seedScope(); + await insertRecord(identity, "stored", Date.now() + 3_600_000); + const store = createDbSetupTokenCleanupStore(db); + + const lockDb = createDb(connectionString); + let signalLocked!: () => void; + const locked = new Promise((resolve) => { + signalLocked = resolve; + }); + let releaseGate!: () => void; + const gate = new Promise((resolve) => { + releaseGate = resolve; + }); + const lockHeld = lockDb.transaction(async (tx) => { + await tx.execute( + sql`SELECT bound_at FROM claude_setup_token_sessions WHERE session_id = ${identity.sessionId} FOR UPDATE`, + ); + // Set a near deadline inside the locked transaction. The consume cannot see + // this value until the transaction commits. + await tx.execute( + sql`UPDATE claude_setup_token_sessions SET deadline_at = clock_timestamp() + interval '300 milliseconds' WHERE session_id = ${identity.sessionId}`, + ); + signalLocked(); + await gate; + }); + + // Wait until the lock is truly held, then fire the consume. The consume + // blocks on the row lock; its initial scan sees the far-future deadline. + await locked; + const consumePromise = store.consumeStoredClaim(identity); + // Hold the lock until the near deadline passes, then commit. + await delay(600); + releaseGate(); + await lockHeld; + + const consumed = await consumePromise; + expect(consumed).toBeNull(); + expect((await readRow(identity.sessionId))?.boundAt).toBeNull(); + await lockDb.$client.end(); + }); +}); diff --git a/server/src/services/setup-token-session.ts b/server/src/services/setup-token-session.ts index d57ab21213..617d02d295 100644 --- a/server/src/services/setup-token-session.ts +++ b/server/src/services/setup-token-session.ts @@ -33,12 +33,23 @@ // testable. import { randomBytes } from "node:crypto"; +import { and, eq, gt, inArray, isNotNull, isNull, lte, or, sql } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { claudeSetupTokenSessions } from "@paperclipai/db"; +import type { AgentAdapterType } from "@paperclipai/shared"; -/** The session states. The four terminal states end the login. */ +/** + * The session states. The four terminal states end the login. The `stored` + * state is not terminal: a session enters `stored` after a successful + * owner-bound secret write, and the durable row then persists as a one-time + * claim. The create path consumes the claim, or the reaper removes the row after + * the deadline. + */ export type SetupTokenSessionState = | "starting" | "awaiting_code" | "submitting" + | "stored" | "completed" | "failed" | "timed_out" @@ -57,14 +68,31 @@ export function isTerminalSessionState(state: SetupTokenSessionState): boolean { } /** - * The immutable owner scope of a session. Every operation verifies all three + * The immutable owner scope of a session. Every operation verifies all five * fields against the stored scope. The service builds the scope once at start * and never changes it. + * + * The `targetAgentId` is null for a company-and-environment login. That login + * has no agent id: a hire flow starts one session before an agent exists. The + * agent-scoped login sets `targetAgentId` to the agent id. The full-scope match + * treats null as one immutable value, so a company login and an agent login + * never resolve one another. */ export interface SetupTokenSessionScope { companyId: string; ownerUserId: string; - targetAgentId: string; + targetAgentId: string | null; + // The adapter and the environment of the login. The durable cleanup record + // keys on the company, the owner, the adapter, and the environment, so a hire + // flow with no agent id still resolves one record. + adapterType: string; + environmentId: string; + // The optional confirmed-overwrite capture. The client sends it when a stored + // token fails the agent test. The secret writer reads it and rotates the + // stored value under the captured version instead of a first write. It is not + // part of the session identity: the non-start routes rebuild the scope without + // it, so the full-scope match ignores it (see `resolveOwned`). + confirmedOverwrite?: { expectedSecretId: string; expectedLatestVersion: number } | null; } /** The terminal outcome of the live login process. */ @@ -95,16 +123,37 @@ export type SetupTokenPromptSink = (prompt: { url: string }) => void; /** * The credential sink the factory calls one time when the login binds the minted - * token from the success record. The service holds the token in memory only and - * returns it one time through receive-token. The sink never logs the token. + * token from the success record. The sink is asynchronous: it awaits the + * owner-bound secret write and rejects on a storage failure. The factory (or the + * runner it wraps) awaits this sink before it reports success, so a storage + * failure ends the login as a failure. The sink never logs the token and never + * returns it. */ -export type SetupTokenCredentialSink = (token: string) => void; +export type SetupTokenCredentialSink = (token: string) => Promise; + +/** + * The atomic credential-claim writer. The service awaits it one time with the + * minted token, the owner scope, and the durable session id. It runs one + * control-plane transaction that first transitions the exact durable row to + * `stored` with a full-scope conditional update, then performs the owner-bound + * secret compare-and-set on the same transaction handle. The commit establishes + * the encrypted secret and the `stored` claim together. It resolves on a + * successful commit. It rejects and rolls back the whole transaction on a + * zero-row transition or on a storage failure, so neither the secret nor the + * claim commits. The service holds the token only for this call; it never stores + * it. + */ +export type SetupTokenSecretWriter = (input: { + scope: SetupTokenSessionScope; + sessionId: string; + token: string; +}) => Promise; /** * Builds the live login process for one session. The factory receives the * prompt sink, the credential sink, the host timeout, and an abort signal that - * the service aborts on cancel and on expiry. The factory delivers the token one - * time through the credential sink. + * the service aborts on cancel and on expiry. The factory awaits the credential + * sink one time before it reports success. */ export type SetupTokenLoginProcessFactory = (params: { scope: SetupTokenSessionScope; @@ -132,31 +181,72 @@ export interface SetupTokenLeaseManager { } /** - * The non-secret cleanup record. It holds only ids, the deadline, and the - * terminal state. It never holds a URL, a code, a token, or a raw process chunk - * (SR-4). The service persists it at start so a restart can reap the lease. + * The non-secret cleanup record. It holds only ids, the deadline, the claim + * marker, and the state. It never holds a URL, a code, a token, or a raw process + * chunk. The service persists it at start so a restart can reap the + * lease. The record keys on the company, the owner, the adapter, and the + * environment, not the agent id, so a hire flow with no agent still resolves it. */ export interface SetupTokenCleanupRecord { sessionId: string; companyId: string; ownerUserId: string; - targetAgentId: string; + adapterType: string; + environmentId: string; leaseId: string; deadline: number; state: SetupTokenSessionState; + // The claim-consumption marker. It is null while a `stored` claim is live. The + // create path sets it one time when it consumes the claim. + boundAt: number | null; +} + +/** + * The immutable identity of a cleanup record. It is the full owner scope plus + * the session id. Every durable write matches on all five fields, so a write + * never updates a row by session id alone. + */ +export interface SetupTokenCleanupIdentity { + sessionId: string; + companyId: string; + ownerUserId: string; + adapterType: string; + environmentId: string; } /** * The durable store for the non-secret cleanup record. A restart reads the - * store and reaps a lease whose session is terminal or past its deadline. The - * store persists no secret. + * store and reaps a lease whose session is terminal, past its deadline, or + * already consumed. The store persists no secret. */ export interface SetupTokenCleanupStore { record(record: SetupTokenCleanupRecord): Promise; - markState(sessionId: string, state: SetupTokenSessionState): Promise; - remove(sessionId: string): Promise; - /** Returns each record whose session is terminal or whose deadline is past. */ + /** + * Marks the state only when the full owner scope and the session id match. The + * write never updates a row by the session id alone. + */ + markState(identity: SetupTokenCleanupIdentity, state: SetupTokenSessionState): Promise; + /** + * Deletes the record only when the full owner scope and the session id match. + * The delete never removes a row by the session id alone, so a cross-scope + * caller cannot delete a foreign row. + */ + remove(identity: SetupTokenCleanupIdentity): Promise; + /** + * Returns each record whose session is terminal, whose deadline is past, or + * whose claim is already consumed. + */ listReapable(now: number): Promise; + /** + * Consumes a `stored` claim with one conditional write. The predicate carries + * the full owner scope, the session id, `state = stored`, an unexpired + * deadline that it checks with `clock_timestamp()`, and an unconsumed marker. + * The write sets `bound_at` one time and returns the row on a valid consume. + * It returns null for a missing, foreign-scope, expired, non-`stored`, or + * already-consumed claim. It never reads the state or the expiry in a separate + * step. The agent-service transaction calls this method. + */ + consumeStoredClaim(identity: SetupTokenCleanupIdentity): Promise; } /** A per-key start rate limiter. It matches the invite-rate-limit shape. */ @@ -194,12 +284,21 @@ export const SETUP_TOKEN_SESSION_NOT_FOUND = "Setup-token login session not foun export const SETUP_TOKEN_SUBMIT_CONFLICT = "The setup-token login session cannot accept this code."; export const SETUP_TOKEN_RATE_LIMITED = "Too many setup-token login attempts. Try again later."; export const SETUP_TOKEN_CAP_EXCEEDED = "Too many active setup-token login sessions."; -export const SETUP_TOKEN_TRANSPORT_INSECURE = - "This response requires a secure transport and is not available on this request."; -// The fixed error for a token that is not ready, or that the owner already -// received. It never tells the two cases apart, so it leaks no session state. +// The fixed error for a completion that is not ready. The session is not +// `completed` with a stored secret yet. It leaks no session state. export const SETUP_TOKEN_TOKEN_UNAVAILABLE = "The setup-token is not available for this session."; export const SETUP_TOKEN_START_FAILED = "The setup-token login session could not start."; +// The fixed error for a sandbox provider that does not advertise the setup-token +// login capability. Only a provider that implements the setup-token +// pseudo-terminal methods can host the login. The route returns this specific, +// typed error and starts no session, so an unsupported provider never reaches a +// session row, a lease, or a pseudo-terminal. +export const SETUP_TOKEN_PROVIDER_UNSUPPORTED = + "The sandbox provider does not support the Claude setup-token login."; +export const SETUP_TOKEN_PROVIDER_UNSUPPORTED_CODE = "setup_token_provider_unsupported"; +// The fixed error for a failed owner-bound secret write. It carries no token and +// no storage detail, so it leaks no secret. +export const SETUP_TOKEN_STORAGE_FAILED = "The setup-token login could not store the credential."; /** A typed error the route maps to a fixed status and the fixed text above. */ export class SetupTokenSessionError extends Error { @@ -391,6 +490,21 @@ export function assessConfidentialStartup(config: ConfidentialTransportConfig): // --- The session service ----------------------------------------------------- +/** + * A synchronous capacity hold for the enforced caps. The service reserves the + * capacity for the owner, the agent, and the company before the first `await` in + * {@link SetupTokenSessionService.start}, so two concurrent starts for one owner + * never both pass the cap. The service holds the reservation for the whole + * session lifetime and releases it exactly one time: on an early start failure or + * on the terminal cleanup. The `released` flag makes the release idempotent. + */ +interface CapReservation { + released: boolean; + companyId: string; + ownerUserId: string; + targetAgentId: string | null; +} + interface StoredSession { id: string; scope: SetupTokenSessionScope; @@ -402,15 +516,25 @@ interface StoredSession { // The full login URL. The service holds it in memory only and returns it only // through the authorized owner read response (SR-5). loginUrl: string | null; - // The minted token. The service holds it in memory only, from the credential - // sink until the authorized owner receives it one time or the retention window - // ends (SR-1, SR-4). - token: string | null; + // True after the owner-bound secret write succeeds. The service never holds the + // token: the credential sink writes it to the secret store and the service + // records only this non-secret marker. + secretStored: boolean; timer: ReturnType | null; - // The retention timer for a completed token. The service arms it after a - // successful login and clears it when the owner receives the token. + // The retention timer for a completed, stored session. The service arms it + // after a successful secret write and clears it when the owner reads the + // completion, so the in-memory session drops even if the owner never reads it. retentionTimer: ReturnType | null; cleanupDone: boolean; + // The per-session mutex tail. It serializes the owner-bound secret write + // against a cancel, an expiry, and the process-done transition, so a terminal + // transition and the write never interleave (complete mediation). The service + // holds this lock across the awaited secret write, so a cancel or an expiry + // that arrives during the write waits for the write to settle first. + lock: Promise; + // The synchronous capacity hold for the enforced caps. The cleanup releases it + // exactly one time when the session reaches a terminal state. + reservation: CapReservation; } /** The public read view of a session prompt. */ @@ -420,10 +544,43 @@ export interface SetupTokenPromptView { loginUrl: string | null; } +/** + * The owner descriptor of a session. The company-and-environment routes read it + * to build the response contract. It carries the immutable environment and the + * session deadline, plus the current state and the full login URL. The route + * projects it: the public status response drops the login URL; the owner prompt + * response returns the login URL through the confidential transport guard. + */ +export interface SetupTokenSessionDescriptor { + sessionId: string; + state: SetupTokenSessionState; + environmentId: string; + /** The session deadline in epoch milliseconds. */ + deadline: number; + /** The full login URL, present only after the prompt surfaces. */ + loginUrl: string | null; +} + +/** + * The completion contract for the authorized owner. It carries the non-secret + * `storedSessionId` claim and no token. The `storedSessionId` is the + * durable session id; the agent-create transaction consumes it as the one-time + * stored-session claim. + */ +export interface SetupTokenCompletionView { + storedSessionId: string; +} + export interface SetupTokenSessionServiceOptions { factory: SetupTokenLoginProcessFactory; leases: SetupTokenLeaseManager; store: SetupTokenCleanupStore; + /** + * The owner-bound secret writer. The service awaits it one time when the login + * binds the token. It performs the compare-and-set. The service fails + * closed on a rejection. + */ + completeCredential: SetupTokenSecretWriter; rateLimiter: SetupTokenRateLimiter; caps?: SetupTokenSessionCaps; ttlMs?: number; @@ -447,9 +604,16 @@ function defaultSessionId(): string { */ export class SetupTokenSessionService { private readonly sessions = new Map(); + // The live reservation counts per owner, per agent, and per company. The start + // path reads and increments these synchronously before the first `await`, so + // the cap decision and the capacity hold are one atomic step on the event loop. + private readonly reservedByOwner = new Map(); + private readonly reservedByAgent = new Map(); + private readonly reservedByCompany = new Map(); private readonly factory: SetupTokenLoginProcessFactory; private readonly leases: SetupTokenLeaseManager; private readonly store: SetupTokenCleanupStore; + private readonly completeCredential: SetupTokenSecretWriter; private readonly rateLimiter: SetupTokenRateLimiter; private readonly caps: SetupTokenSessionCaps; private readonly ttlMs: number; @@ -462,6 +626,7 @@ export class SetupTokenSessionService { this.factory = options.factory; this.leases = options.leases; this.store = options.store; + this.completeCredential = options.completeCredential; this.rateLimiter = options.rateLimiter; this.caps = options.caps ?? DEFAULT_SETUP_TOKEN_SESSION_CAPS; this.ttlMs = options.ttlMs ?? DEFAULT_SETUP_TOKEN_SESSION_TTL_MS; @@ -471,6 +636,43 @@ export class SetupTokenSessionService { this.log = options.log ?? (() => {}); } + /** + * Builds the durable-record identity for a session. The store matches every + * write on the full owner scope and the session id, so no write updates a row + * by the session id alone. + */ + private identityOf(session: StoredSession): SetupTokenCleanupIdentity { + return { + sessionId: session.id, + companyId: session.scope.companyId, + ownerUserId: session.scope.ownerUserId, + adapterType: session.scope.adapterType, + environmentId: session.scope.environmentId, + }; + } + + /** + * Runs `fn` under the per-session lock. The lock serializes the owner-bound + * secret write against a cancel, an expiry, and the process-done transition, so + * a terminal transition and the write never interleave (complete mediation). + * The caller inside `fn` must not call another locked method, or it deadlocks; + * the failure path of {@link onCredential} calls {@link terminateLocked}, the + * lock-free variant, for this reason. + */ + private async withSessionLock(session: StoredSession, fn: () => Promise): Promise { + const prior = session.lock; + let release: () => void = () => {}; + session.lock = new Promise((resolve) => { + release = resolve; + }); + await prior; + try { + return await fn(); + } finally { + release(); + } + } + private countActive(predicate: (session: StoredSession) => boolean): number { let count = 0; for (const session of this.sessions.values()) { @@ -479,6 +681,73 @@ export class SetupTokenSessionService { return count; } + private static incrementCount(counts: Map, key: string): void { + counts.set(key, (counts.get(key) ?? 0) + 1); + } + + private static decrementCount(counts: Map, key: string): void { + const next = (counts.get(key) ?? 0) - 1; + if (next > 0) { + counts.set(key, next); + } else { + counts.delete(key); + } + } + + /** + * Reserves the capacity for one start under every enforced cap. The method is + * synchronous, so it runs to completion before the first `await` in + * {@link start}. Two concurrent starts for one owner cannot interleave inside + * it: the first reserves the owner slot, and the second reads the incremented + * count and fails closed with the fixed 429 cap error. The method holds the + * per-owner, per-agent, and per-company semantics: it counts the agent slot + * only for an agent-scoped login, because a null agent id is not a shared key + * across owners. It increments no counter on a rejection, so a rejected start + * reserves nothing. + */ + private reserveCapacity(scope: SetupTokenSessionScope): CapReservation { + if ((this.reservedByOwner.get(scope.ownerUserId) ?? 0) >= this.caps.perOwner) { + throw new SetupTokenSessionError(429, SETUP_TOKEN_CAP_EXCEEDED); + } + if ( + scope.targetAgentId !== null && + (this.reservedByAgent.get(scope.targetAgentId) ?? 0) >= this.caps.perAgent + ) { + throw new SetupTokenSessionError(429, SETUP_TOKEN_CAP_EXCEEDED); + } + if ((this.reservedByCompany.get(scope.companyId) ?? 0) >= this.caps.perCompany) { + throw new SetupTokenSessionError(429, SETUP_TOKEN_CAP_EXCEEDED); + } + SetupTokenSessionService.incrementCount(this.reservedByOwner, scope.ownerUserId); + if (scope.targetAgentId !== null) { + SetupTokenSessionService.incrementCount(this.reservedByAgent, scope.targetAgentId); + } + SetupTokenSessionService.incrementCount(this.reservedByCompany, scope.companyId); + return { + released: false, + companyId: scope.companyId, + ownerUserId: scope.ownerUserId, + targetAgentId: scope.targetAgentId, + }; + } + + /** + * Releases a capacity reservation exactly one time. The `released` flag makes + * the release idempotent, so an early start failure and the terminal cleanup + * never double-release one reservation. The method decrements the same counters + * that {@link reserveCapacity} incremented, and it drops the agent counter only + * for an agent-scoped login. + */ + private releaseReservation(reservation: CapReservation): void { + if (reservation.released) return; + reservation.released = true; + SetupTokenSessionService.decrementCount(this.reservedByOwner, reservation.ownerUserId); + if (reservation.targetAgentId !== null) { + SetupTokenSessionService.decrementCount(this.reservedByAgent, reservation.targetAgentId); + } + SetupTokenSessionService.decrementCount(this.reservedByCompany, reservation.companyId); + } + /** * Starts a login session. It rate-limits the start, enforces the caps, * acquires the lease with an external expiry no later than the deadline, @@ -490,29 +759,45 @@ export class SetupTokenSessionService { if (!rate.allowed) { throw new SetupTokenSessionError(429, SETUP_TOKEN_RATE_LIMITED); } - if (this.countActive((s) => s.scope.ownerUserId === scope.ownerUserId) >= this.caps.perOwner) { - throw new SetupTokenSessionError(429, SETUP_TOKEN_CAP_EXCEEDED); - } - if (this.countActive((s) => s.scope.targetAgentId === scope.targetAgentId) >= this.caps.perAgent) { - throw new SetupTokenSessionError(429, SETUP_TOKEN_CAP_EXCEEDED); - } - if (this.countActive((s) => s.scope.companyId === scope.companyId) >= this.caps.perCompany) { - throw new SetupTokenSessionError(429, SETUP_TOKEN_CAP_EXCEEDED); - } + // Reserve the capacity synchronously before the first `await`. This closes + // the time-of-check/time-of-use window: the lease acquire, the durable write, + // and the factory creation all run after the reservation, so two concurrent + // starts for one owner cannot both pass the cap. The service releases the + // reservation on every early failure below and on the terminal cleanup. + const reservation = this.reserveCapacity(scope); const sessionId = this.generateSessionId(); const deadline = this.now() + this.ttlMs; - const lease = await this.leases.acquire({ scope, deadline }); - await this.store.record({ - sessionId, - companyId: scope.companyId, - ownerUserId: scope.ownerUserId, - targetAgentId: scope.targetAgentId, - leaseId: lease.id, - deadline, - state: "starting", - }); + let lease: SetupTokenLease; + try { + lease = await this.leases.acquire({ scope, deadline }); + } catch (error) { + // The lease acquire failed before any durable state exists. Roll back the + // reservation and rethrow the original error. + this.releaseReservation(reservation); + throw error; + } + + try { + await this.store.record({ + sessionId, + companyId: scope.companyId, + ownerUserId: scope.ownerUserId, + adapterType: scope.adapterType, + environmentId: scope.environmentId, + leaseId: lease.id, + deadline, + state: "starting", + boundAt: null, + }); + } catch (error) { + // The durable write failed, so no record exists for the reaper to read. + // Release the lease at once, roll back the reservation, and rethrow. + await this.releaseLeaseSafely(lease); + this.releaseReservation(reservation); + throw error; + } const abort = new AbortController(); const session: StoredSession = { @@ -525,10 +810,12 @@ export class SetupTokenSessionService { process: undefined as unknown as SetupTokenLoginProcess, abort, loginUrl: null, - token: null, + secretStored: false, timer: null, retentionTimer: null, cleanupDone: false, + lock: Promise.resolve(), + reservation, }; let process: SetupTokenLoginProcess; @@ -541,10 +828,20 @@ export class SetupTokenSessionService { signal: abort.signal, }); } catch { - // The factory could not start the process. Release the lease and drop the - // durable record, then return a fixed, non-secret error. + // The factory could not start the process. Release the lease, drop the + // durable record, roll back the reservation, then return a fixed, + // non-secret error. await this.releaseLeaseSafely(lease); - await this.store.remove(sessionId).catch(() => {}); + await this.store + .remove({ + sessionId, + companyId: scope.companyId, + ownerUserId: scope.ownerUserId, + adapterType: scope.adapterType, + environmentId: scope.environmentId, + }) + .catch(() => {}); + this.releaseReservation(reservation); throw new SetupTokenSessionError(503, SETUP_TOKEN_START_FAILED); } @@ -571,26 +868,75 @@ export class SetupTokenSessionService { session.loginUrl = prompt.url; if (session.state === "starting") { session.state = "awaiting_code"; - void this.store.markState(session.id, "awaiting_code").catch(() => {}); + void this.store.markState(this.identityOf(session), "awaiting_code").catch(() => {}); } } /** - * Receives the minted token from the login process. The service holds the - * token in memory only, until the authorized owner receives it one time or the - * retention window ends. It never logs the token (SR-1). It ignores a token - * that arrives after a non-success terminal state. + * Receives the minted token from the login process and writes it to the + * owner-bound secret. The service awaits the compare-and-set. The + * service never stores the token: it passes the token to the writer, then it + * drops the reference. + * + * The service runs the whole sink under the per-session lock, so a cancel or an + * expiry cannot interleave with the write. A cancel or an expiry that wins the + * lock first sets a terminal state; the service then writes no secret and fails + * closed with the fixed, non-secret error. This is complete mediation: a + * terminal session never commits a secret write. + * + * The writer runs one control-plane transaction. Inside it, the writer first + * transitions the exact durable row to `stored` with a full-scope conditional + * update, then writes or rotates the secret on the same transaction handle. The + * commit establishes the encrypted secret and the `stored` claim together, so a + * crash between the two steps cannot leave a stored token with no valid claim. + * The service does not mark the durable row separately; the writer owns the + * transition. + * + * On a successful write the service records the non-secret `secretStored` + * marker. The process outcome then moves the session to `completed`. A cancel or + * an expiry that arrives after the write committed sees the `secretStored` + * marker and reports the completed state; it does not erase the claim (see + * {@link terminateLocked}). + * + * On a zero-row transition or on a storage failure the writer rolls back the + * whole transaction and rejects. The service then fails closed: it moves the + * session to `failed`, keeps no secret and no claim, and rejects with a fixed, + * non-secret error. The factory (or the runner it wraps) awaits this sink, so + * the rejection ends the login as a failure and the process never reports + * success. */ - private onCredential(session: StoredSession, token: string): void { - if (isTerminalSessionState(session.state) && session.state !== "completed") return; - session.token = token; + private async onCredential(session: StoredSession, token: string): Promise { + await this.withSessionLock(session, async () => { + // A cancel or an expiry won the lock first and set a terminal state. Do not + // write the secret for a terminal session. Fail closed with the fixed, + // non-secret error, so the runner ends the run as a failure. + if (isTerminalSessionState(session.state)) { + throw new SetupTokenSessionError(500, SETUP_TOKEN_STORAGE_FAILED); + } + try { + // The writer transitions the durable row to `stored` and writes the + // secret in one transaction. A zero-row transition or a storage failure + // rejects and rolls back both. + await this.completeCredential({ scope: session.scope, sessionId: session.id, token }); + } catch { + await this.terminateLocked(session, "failed"); + throw new SetupTokenSessionError(500, SETUP_TOKEN_STORAGE_FAILED); + } + // The transaction committed while the service held the lock, so a cancel or + // an expiry could not interleave. Record the non-secret marker. The durable + // row is already `stored` from the committed transition. + session.secretStored = true; + }); } /** * Resolves a session for an operation. It returns the session only when the - * id exists and the stored scope equals the caller scope in all three fields. - * A missing session and a cross-scope session both throw the same not-found - * error, so a caller cannot tell them apart (SR-3). + * id exists and the stored scope equals the caller scope in all five fields: + * the company, the owner, the agent, the adapter, and the environment. A + * missing session and a cross-scope session both throw the same not-found + * error, so a caller cannot tell them apart. The full-scope match makes + * a cross-adapter and a cross-environment session return the same not-found + * error as a missing session. */ private resolveOwned(sessionId: string, scope: SetupTokenSessionScope): StoredSession { const session = this.sessions.get(sessionId); @@ -598,13 +944,65 @@ export class SetupTokenSessionService { !session || session.scope.companyId !== scope.companyId || session.scope.ownerUserId !== scope.ownerUserId || - session.scope.targetAgentId !== scope.targetAgentId + session.scope.targetAgentId !== scope.targetAgentId || + session.scope.adapterType !== scope.adapterType || + session.scope.environmentId !== scope.environmentId ) { throw new SetupTokenSessionError(404, SETUP_TOKEN_SESSION_NOT_FOUND); } return session; } + /** + * Resolves the immutable scope of a company-and-environment session. The + * caller provides the company, the owner, and the adapter it derived from the + * request; the route path gives the company, the actor gives the owner, and + * the route fixes the adapter. The lookup matches these three fields and the + * agentless marker, then returns the full scope, including the intrinsic + * environment. + * + * A caller never supplies the environment on a read, a submit, a cancel, or a + * completion. The environment is intrinsic to the session, so a foreign + * environment cannot address the session. A missing session and a + * cross-company, cross-owner, or cross-adapter session all throw the same + * not-found error. The agentless marker rejects an + * agent-scoped session, so a company route never resolves an agent session. + */ + resolveCompanyScope( + sessionId: string, + key: { companyId: string; ownerUserId: string; adapterType: string }, + ): SetupTokenSessionScope { + const session = this.sessions.get(sessionId); + if ( + !session || + session.scope.targetAgentId !== null || + session.scope.companyId !== key.companyId || + session.scope.ownerUserId !== key.ownerUserId || + session.scope.adapterType !== key.adapterType + ) { + throw new SetupTokenSessionError(404, SETUP_TOKEN_SESSION_NOT_FOUND); + } + return session.scope; + } + + /** + * Returns the owner descriptor for a session. The company-and-environment + * routes read it to build the response contract. The scope check runs first, + * so a cross-scope caller gets the same not-found error. The route + * projects the descriptor: the public status response drops the login URL; the + * owner prompt response returns the login URL behind the transport guard. + */ + describeOwned(sessionId: string, scope: SetupTokenSessionScope): SetupTokenSessionDescriptor { + const session = this.resolveOwned(sessionId, scope); + return { + sessionId: session.id, + state: session.state, + environmentId: session.scope.environmentId, + deadline: session.deadline, + loginUrl: session.loginUrl, + }; + } + /** * Returns the prompt view for the authorized owner. The full login URL rides * only in this response (SR-5). The route sets `Cache-Control: no-store` and @@ -627,7 +1025,7 @@ export class SetupTokenSessionService { throw new SetupTokenSessionError(409, SETUP_TOKEN_SUBMIT_CONFLICT); } session.state = "submitting"; - void this.store.markState(session.id, "submitting").catch(() => {}); + void this.store.markState(this.identityOf(session), "submitting").catch(() => {}); session.process.submitCode(code); return { state: session.state }; } @@ -666,62 +1064,81 @@ export class SetupTokenSessionService { } /** - * Returns the token for the authorized owner one time. The service returns the - * token only from a completed session that still holds it. It clears the token - * and drops the retained session at once, so a second receive returns the same - * not-found error as a missing session. It returns the fixed unavailable error - * when the token is not ready or the owner already received it. The route - * applies the confidential transport guard and sets `Cache-Control: no-store` - * before it calls this method. + * Returns the completion contract for the authorized owner. It returns the + * non-secret `storedSessionId` claim only from a completed session whose + * owner-bound secret write succeeded. It returns no token. It + * returns the fixed unavailable error when the session is not completed with a + * stored secret. The scope check runs first, so a cross-scope caller gets the + * same not-found error, not the unavailable error. + * + * The read leaves the durable row in place as the one-time stored-session + * claim; the agent-create transaction consumes it. The route sets + * `Cache-Control: no-store` before it calls this method. */ - receiveToken(sessionId: string, scope: SetupTokenSessionScope): { token: string } { - // The scope check still runs, so a cross-scope caller gets the same - // not-found error, not the unavailable error. + completeSession(sessionId: string, scope: SetupTokenSessionScope): SetupTokenCompletionView { const session = this.resolveOwned(sessionId, scope); - if (session.state !== "completed" || session.token === null) { + if (session.state !== "completed" || !session.secretStored) { throw new SetupTokenSessionError(409, SETUP_TOKEN_TOKEN_UNAVAILABLE); } - const token = session.token; - // One-shot delivery: clear the token and drop the retained session, so the - // service never returns the token a second time (SR-1). - this.purgeRetained(session.id); - return { token }; + return { storedSessionId: session.id }; } /** - * Stops the direct child, then runs the idempotent cleanup. The order stops - * the child before the lease release on every terminal path (SR-4). + * Stops the direct child, then runs the idempotent cleanup under the + * per-session lock. The lock serializes the terminal transition against the + * owner-bound secret write, so a cancel or an expiry never interleaves with the + * write (complete mediation). The order stops the child before the lease + * release on every terminal path. */ private async terminate(session: StoredSession, state: SetupTokenSessionState): Promise { + await this.withSessionLock(session, () => this.terminateLocked(session, state)); + } + + /** + * The lock-free terminal transition. The caller must already hold the + * per-session lock. The failure path of {@link onCredential} calls it directly, + * because that path already holds the lock. + * + * If the owner-bound secret write already committed, the delivery won the + * serialized ordering. The service completes the session and keeps the stored + * claim; it does not cancel, expire, or erase the claim. This makes the + * terminal API report the completed state after a successful write, rather than + * erase it. + */ + private async terminateLocked(session: StoredSession, state: SetupTokenSessionState): Promise { if (isTerminalSessionState(session.state)) return; - session.state = state; + const resolved: SetupTokenSessionState = session.secretStored ? "completed" : state; + session.state = resolved; session.abort.abort(); try { session.process.stop(); } catch { this.log("[paperclip] Setup-token session: the process stop step errored."); } - await this.runCleanup(session, state); + await this.runCleanup(session, resolved); } /** - * Maps the live process outcome to the terminal state and runs the cleanup. - * A cancel or an expire already set the state, so this call is a no-op then. + * Maps the live process outcome to the terminal state and runs the cleanup + * under the per-session lock. A cancel or an expire already set the state, so + * this call is a no-op then. */ private async onProcessDone(session: StoredSession, outcome: SetupTokenLoginOutcome): Promise { - if (isTerminalSessionState(session.state) && session.cleanupDone) return; - const state: SetupTokenSessionState = - outcome === "success" - ? "completed" - : outcome === "timeout" - ? "timed_out" - : outcome === "cancelled" - ? "cancelled" - : "failed"; - if (!isTerminalSessionState(session.state)) { - session.state = state; - } - await this.runCleanup(session, session.state); + await this.withSessionLock(session, async () => { + if (isTerminalSessionState(session.state) && session.cleanupDone) return; + const state: SetupTokenSessionState = + outcome === "success" + ? "completed" + : outcome === "timeout" + ? "timed_out" + : outcome === "cancelled" + ? "cancelled" + : "failed"; + if (!isTerminalSessionState(session.state)) { + session.state = state; + } + await this.runCleanup(session, session.state); + }); } /** @@ -730,50 +1147,58 @@ export class SetupTokenSessionService { * session. It clears the in-memory login URL reference promptly (SR-3, SR-5). * A cleanup failure stays retryable and records no process output (SR-4). * - * A completed session that still holds a token stays in memory until the - * authorized owner receives it one time or the retention window ends. The - * cleanup still releases the sandbox lease at once; it only holds the token in - * memory, not the sandbox (SR-4). + * A completed session with a stored secret keeps its durable row as the + * one-time stored-session claim: the cleanup marks no terminal state and does + * not remove the row. The agent-create transaction consumes the claim, or the + * reaper removes the row after the deadline. The cleanup still releases the + * sandbox lease at once. It retains the in-memory session for the retention + * window, so the owner can read the completion once. */ private async runCleanup(session: StoredSession, state: SetupTokenSessionState): Promise { if (session.cleanupDone) return; session.cleanupDone = true; + // Release the capacity reservation as the session reaches a terminal state. + // A terminal session no longer counts against a cap, so the owner regains the + // slot at once, even while a completed session lingers for the retention read. + this.releaseReservation(session.reservation); if (session.timer) { clearTimeout(session.timer); session.timer = null; } // Clear the secret-bearing reference promptly. Best-effort only (SR-5). session.loginUrl = null; - try { - await this.store.markState(session.id, state); - } catch { - this.log("[paperclip] Setup-token session: the cleanup record update failed; it stays retryable."); + const storedClaim = state === "completed" && session.secretStored; + if (!storedClaim) { + try { + await this.store.markState(this.identityOf(session), state); + } catch { + this.log("[paperclip] Setup-token session: the cleanup record update failed; it stays retryable."); + } } await this.releaseLeaseSafely(session.lease); - try { - await this.store.remove(session.id); - } catch { - this.log("[paperclip] Setup-token session: the cleanup record removal failed; it stays retryable."); - } - if (state === "completed" && session.token !== null) { - // Retain the token in memory for the owner to receive it one time. The - // retention timer purges it if the owner never receives it (SR-4). + if (storedClaim) { + // Keep the durable row as the stored-session claim. Retain the in-memory + // session for the owner to read the completion one time. session.retentionTimer = setTimeout(() => this.purgeRetained(session.id), this.tokenRetentionMs); if (typeof session.retentionTimer.unref === "function") session.retentionTimer.unref(); return; } + try { + await this.store.remove(this.identityOf(session)); + } catch { + this.log("[paperclip] Setup-token session: the cleanup record removal failed; it stays retryable."); + } this.sessions.delete(session.id); } /** - * Purges a retained completed session. It clears the token reference, clears - * the retention timer, and drops the in-memory session. The owner receive path - * and the retention timer both call it (SR-1, SR-4). + * Purges a retained completed session from memory. It clears the retention + * timer and drops the in-memory session. The durable stored-session claim + * stays in the store for the create flow or the reaper. */ private purgeRetained(sessionId: string): void { const session = this.sessions.get(sessionId); if (!session) return; - session.token = null; if (session.retentionTimer) { clearTimeout(session.retentionTimer); session.retentionTimer = null; @@ -804,7 +1229,13 @@ export class SetupTokenSessionService { for (const record of records) { try { await this.leases.releaseById(record.leaseId); - await this.store.remove(record.sessionId); + await this.store.remove({ + sessionId: record.sessionId, + companyId: record.companyId, + ownerUserId: record.ownerUserId, + adapterType: record.adapterType, + environmentId: record.environmentId, + }); released += 1; } catch { failed += 1; @@ -830,3 +1261,161 @@ export class SetupTokenSessionService { return this.countActive(() => true); } } + +// --- The durable, database-backed cleanup store ------------------------------ + +type ClaudeSetupTokenSessionRow = typeof claudeSetupTokenSessions.$inferSelect; + +/** Maps a database row to the non-secret cleanup record. */ +function toCleanupRecord(row: ClaudeSetupTokenSessionRow): SetupTokenCleanupRecord { + return { + sessionId: row.sessionId, + companyId: row.companyId, + ownerUserId: row.ownerUserId, + adapterType: row.adapterType, + environmentId: row.environmentId, + leaseId: row.leaseId ?? "", + deadline: row.deadlineAt.getTime(), + state: row.state as SetupTokenSessionState, + boundAt: row.boundAt ? row.boundAt.getTime() : null, + }; +} + +/** + * The database scope predicate. It matches the full owner scope and the session + * id, so no write ever addresses a row by the session id alone. + */ +function setupTokenScopeMatch(identity: SetupTokenCleanupIdentity) { + return and( + eq(claudeSetupTokenSessions.sessionId, identity.sessionId), + eq(claudeSetupTokenSessions.companyId, identity.companyId), + eq(claudeSetupTokenSessions.ownerUserId, identity.ownerUserId), + eq(claudeSetupTokenSessions.adapterType, identity.adapterType as AgentAdapterType), + eq(claudeSetupTokenSessions.environmentId, identity.environmentId), + ); +} + +/** + * The live pre-`stored` states a session may hold when the credential arrives. + * The atomic claim writer transitions a row to `stored` only from one of these + * states. It never transitions a terminal, an already-`stored`, or a foreign row. + */ +export const SETUP_TOKEN_STORED_PREDECESSOR_STATES: readonly SetupTokenSessionState[] = [ + "starting", + "awaiting_code", + "submitting", +]; + +/** + * Transitions the exact session row to `stored` with one conditional update. The + * predicate matches the full owner scope and the session id, an allowed + * predecessor state, an unexpired deadline against `clock_timestamp()`, and an + * unconsumed `bound_at`. It uses `UPDATE … RETURNING` and returns true only when + * exactly one row transitions. + * + * The atomic claim writer runs this update on the same transaction handle as the + * secret write, so the commit establishes the `stored` claim and the encrypted + * secret together. A zero-row result rolls back the whole transaction, so a + * stale, expired, terminal, foreign-scope, or already-bound row writes no + * secret. + */ +export async function transitionSetupTokenSessionToStored( + executor: Db, + identity: SetupTokenCleanupIdentity, +): Promise { + const changed = await executor + .update(claudeSetupTokenSessions) + .set({ state: "stored", updatedAt: sql`clock_timestamp()` }) + .where( + and( + setupTokenScopeMatch(identity), + inArray(claudeSetupTokenSessions.state, [...SETUP_TOKEN_STORED_PREDECESSOR_STATES]), + gt(claudeSetupTokenSessions.deadlineAt, sql`clock_timestamp()`), + isNull(claudeSetupTokenSessions.boundAt), + ), + ) + .returning(); + return changed.length === 1; +} + +/** + * Builds the durable, database-backed cleanup store. It persists only the + * non-secret record. Every write matches the full owner scope and the session + * id, so no write updates a row by the session id alone. The + * claim-consumption write folds the state check, the deadline check, and the + * consumption into one conditional write. + */ +export function createDbSetupTokenCleanupStore(db: Db): SetupTokenCleanupStore { + // The store keys every scoped write on the full owner scope plus the session id. + const scopeMatch = setupTokenScopeMatch; + + return { + async record(record): Promise { + await db.insert(claudeSetupTokenSessions).values({ + sessionId: record.sessionId, + companyId: record.companyId, + ownerUserId: record.ownerUserId, + adapterType: record.adapterType as AgentAdapterType, + environmentId: record.environmentId, + leaseId: record.leaseId, + state: record.state, + deadlineAt: new Date(record.deadline), + boundAt: record.boundAt === null ? null : new Date(record.boundAt), + }); + }, + + async markState(identity, state): Promise { + // The compare-and-set predicate matches the full owner scope, so a write + // never updates a row by the session id alone. + await db + .update(claudeSetupTokenSessions) + .set({ state, updatedAt: sql`clock_timestamp()` }) + .where(scopeMatch(identity)); + }, + + async remove(identity): Promise { + // The delete matches the full owner scope, so it never removes a row by the + // session id alone. + await db.delete(claudeSetupTokenSessions).where(scopeMatch(identity)); + }, + + async listReapable(now): Promise { + // A record is reapable when its session is terminal, its deadline is past, + // or its claim is already consumed. The deadline index supports the scan. + const rows = await db + .select() + .from(claudeSetupTokenSessions) + .where( + or( + inArray(claudeSetupTokenSessions.state, [...SETUP_TOKEN_TERMINAL_STATES]), + lte(claudeSetupTokenSessions.deadlineAt, new Date(now)), + isNotNull(claudeSetupTokenSessions.boundAt), + ), + ); + return rows.map(toCleanupRecord); + }, + + async consumeStoredClaim(identity): Promise { + // One conditional write. The predicate carries the full owner scope, the + // session id, `state = stored`, an unexpired deadline, and an unconsumed + // marker. It checks the deadline with `clock_timestamp()`, so the database + // evaluates the current time after any row-lock wait. An expired claim + // cannot pass the deadline condition. The write sets `bound_at` one time + // and returns the row only on a valid consume. + const changed = await db + .update(claudeSetupTokenSessions) + .set({ boundAt: sql`clock_timestamp()`, updatedAt: sql`clock_timestamp()` }) + .where( + and( + scopeMatch(identity), + eq(claudeSetupTokenSessions.state, "stored"), + gt(claudeSetupTokenSessions.deadlineAt, sql`clock_timestamp()`), + isNull(claudeSetupTokenSessions.boundAt), + ), + ) + .returning(); + const row = changed[0]; + return row ? toCleanupRecord(row) : null; + }, + }; +} diff --git a/server/src/services/setup-token-transport-binding.test.ts b/server/src/services/setup-token-transport-binding.test.ts new file mode 100644 index 0000000000..eeaaf19b48 --- /dev/null +++ b/server/src/services/setup-token-transport-binding.test.ts @@ -0,0 +1,892 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + buildClaudeOAuthWriteInput, + buildSetupTokenLoginTransport, + createProductionSetupTokenSandboxProvider, + createSetupTokenSecretWriter, + createWorkerBoundSetupTokenPtyOpener, + type SetupTokenSandboxProvider, +} from "./setup-token-transport-binding.js"; +import { + SetupTokenSessionService, + isTerminalSessionState, + type SetupTokenCleanupIdentity, + type SetupTokenCleanupRecord, + type SetupTokenCleanupStore, + type SetupTokenLoginProcess, + type SetupTokenSessionScope, + type SetupTokenSessionState, +} from "./setup-token-session.js"; +import type { SetupTokenPtySessionOpener } from "@paperclipai/adapter-utils/setup-token-transport"; +import { + CLAUDE_SETUP_TOKEN_COMMAND, + SETUP_TOKEN_AFTER_ANCHOR, + SETUP_TOKEN_BEFORE_ANCHOR, +} from "@paperclipai/adapter-claude-local/server"; + +// The owner scope for one login session. The per-owner session cap is one, so one +// scope holds one live session. +const SCOPE: SetupTokenSessionScope = { + companyId: "company-1", + ownerUserId: "user-1", + targetAgentId: null, + adapterType: "claude_local", + environmentId: "env-1", +}; + +/** Waits for the pending microtasks and macrotasks to settle. */ +function flush(): Promise { + return new Promise((resolve) => setImmediate(resolve)); +} + +/** + * A controllable fake sandbox. It records the opened command and every released + * lease id. It drives the login process to a terminal outcome through the exit + * promise, so a test simulates a process failure, a timeout, or a cancel. + */ +function createFakeSandbox() { + const openedCommands: string[] = []; + const releasedLeaseIds: string[] = []; + let acquireCount = 0; + let killed = false; + let resolveExit: (value: { exitCode: number | null }) => void = () => {}; + const exit = new Promise<{ exitCode: number | null }>((resolve) => { + resolveExit = resolve; + }); + + const openPtySession: SetupTokenPtySessionOpener = async (command) => { + openedCommands.push(command); + return { + onData(): void {}, + write(): void {}, + wait: () => exit, + kill(): void { + killed = true; + resolveExit({ exitCode: 137 }); + }, + async close(): Promise {}, + }; + }; + + const provider: SetupTokenSandboxProvider = { + async acquire() { + acquireCount += 1; + return { leaseId: `lease-${acquireCount}`, openPtySession }; + }, + async release(leaseId) { + releasedLeaseIds.push(leaseId); + }, + }; + + return { + provider, + openedCommands, + releasedLeaseIds, + get acquireCount() { + return acquireCount; + }, + get killed() { + return killed; + }, + finishProcess(exitCode: number | null): void { + resolveExit({ exitCode }); + }, + }; +} + +/** + * An in-memory cleanup store that records every write. It stands in for the + * durable database-backed store, so a test asserts the service runs its cleanup + * against the injected store. + */ +function createRecordingStore() { + const rows = new Map(); + const removed: string[] = []; + const stateWrites: Array<{ sessionId: string; state: SetupTokenSessionState }> = []; + let reapable: SetupTokenCleanupRecord[] = []; + + const store: SetupTokenCleanupStore = { + async record(record) { + rows.set(record.sessionId, { ...record }); + }, + async markState(identity: SetupTokenCleanupIdentity, state) { + stateWrites.push({ sessionId: identity.sessionId, state }); + const row = rows.get(identity.sessionId); + if (row) row.state = state; + }, + async remove(identity) { + removed.push(identity.sessionId); + rows.delete(identity.sessionId); + }, + async listReapable() { + return reapable; + }, + async consumeStoredClaim() { + return null; + }, + }; + + return { + store, + rows, + removed, + stateWrites, + setReapable(records: SetupTokenCleanupRecord[]): void { + reapable = records; + }, + }; +} + +/** A permissive rate limiter, so the start path never rate-limits in a test. */ +const allowAllRateLimiter = { consume: () => ({ allowed: true, retryAfterSeconds: 0 }) }; + +function buildService(input: { + sandbox: SetupTokenSandboxProvider; + store: SetupTokenCleanupStore; + ttlMs?: number; +}) { + const transport = buildSetupTokenLoginTransport({ + sandbox: input.sandbox, + store: input.store, + }); + const service = new SetupTokenSessionService({ + factory: transport.factory, + leases: transport.leases, + store: transport.store, + // The secret write stays a fail-closed default here. This phase does not bind + // the owner-bound secret writer. + completeCredential: async () => { + throw new Error("The credential write is not bound in this phase."); + }, + rateLimiter: allowAllRateLimiter, + ttlMs: input.ttlMs ?? 5 * 60_000, + }); + return { service, transport }; +} + +describe("setup-token production transport binding", () => { + it("creates a live session with the transport bound and opens only the fixed command", async () => { + const sandbox = createFakeSandbox(); + const store = createRecordingStore(); + const { service } = buildService({ sandbox: sandbox.provider, store: store.store }); + + const started = await service.start(SCOPE); + await flush(); + + // The start returned a session id instead of the fixed 503. + expect(typeof started.sessionId).toBe("string"); + expect(started.sessionId.length).toBeGreaterThan(0); + // The lease manager acquired one production lease. + expect(sandbox.acquireCount).toBe(1); + // The factory opened the pseudo-terminal with only the fixed command. + expect(sandbox.openedCommands).toEqual([CLAUDE_SETUP_TOKEN_COMMAND]); + // The service persisted the non-secret cleanup record at start. + expect(store.rows.get(started.sessionId)?.leaseId).toBe("lease-1"); + + await service.shutdown(); + }); + + it("releases the lease and cleans the durable store on a process failure", async () => { + const sandbox = createFakeSandbox(); + const store = createRecordingStore(); + const { service } = buildService({ sandbox: sandbox.provider, store: store.store }); + + const started = await service.start(SCOPE); + await flush(); + + // The login process ends with a non-zero exit code. + sandbox.finishProcess(1); + await flush(); + await flush(); + + expect(sandbox.releasedLeaseIds).toEqual(["lease-1"]); + expect(store.removed).toContain(started.sessionId); + expect(store.rows.has(started.sessionId)).toBe(false); + }); + + it("releases the lease and cleans the durable store on a cancel", async () => { + const sandbox = createFakeSandbox(); + const store = createRecordingStore(); + const { service } = buildService({ sandbox: sandbox.provider, store: store.store }); + + const started = await service.start(SCOPE); + await flush(); + + await service.cancel(started.sessionId, SCOPE); + await flush(); + + expect(sandbox.releasedLeaseIds).toEqual(["lease-1"]); + expect(sandbox.killed).toBe(true); + expect(store.removed).toContain(started.sessionId); + }); + + it("releases the lease and cleans the durable store on a timeout expiry", async () => { + const sandbox = createFakeSandbox(); + const store = createRecordingStore(); + const { service } = buildService({ sandbox: sandbox.provider, store: store.store }); + + const started = await service.start(SCOPE); + await flush(); + + await service.expire(started.sessionId, SCOPE); + await flush(); + + expect(sandbox.releasedLeaseIds).toEqual(["lease-1"]); + expect(store.removed).toContain(started.sessionId); + }); + + it("cancels every live session and releases the lease on shutdown", async () => { + const sandbox = createFakeSandbox(); + const store = createRecordingStore(); + const { service } = buildService({ sandbox: sandbox.provider, store: store.store }); + + const started = await service.start(SCOPE); + await flush(); + + await service.shutdown(); + await flush(); + + expect(sandbox.releasedLeaseIds).toEqual(["lease-1"]); + expect(store.removed).toContain(started.sessionId); + expect(service.activeSessionCount()).toBe(0); + }); + + it("ignores a runtime-supplied command and always opens the fixed command", async () => { + const sandbox = createFakeSandbox(); + const store = createRecordingStore(); + // A caller forces an alternate command through the dependency object. The + // dependency type carries no command field, so this cast simulates a + // runtime-supplied value from a route, a body, or a configuration. The + // binding must ignore it and open only the fixed command. + const deps = { + sandbox: sandbox.provider, + store: store.store, + command: "echo pwned", + } as unknown as Parameters[0]; + const transport = buildSetupTokenLoginTransport(deps); + const service = new SetupTokenSessionService({ + factory: transport.factory, + leases: transport.leases, + store: transport.store, + completeCredential: async () => { + throw new Error("The credential write is not bound in this phase."); + }, + rateLimiter: allowAllRateLimiter, + ttlMs: 5 * 60_000, + }); + + const started = await service.start(SCOPE); + await flush(); + + // The factory opened the pseudo-terminal with only the fixed command. The + // supplied alternate value never reached the command. + expect(sandbox.openedCommands).toEqual([CLAUDE_SETUP_TOKEN_COMMAND]); + expect(sandbox.openedCommands).not.toContain("echo pwned"); + + void started; + await service.shutdown(); + }); + + it("keeps the durable cleanup record reapable when a restart release fails", async () => { + // The production provider drives a restart release by id. The provider driver + // release rejects, so the reaper must keep the durable cleanup record. + const releaseRunLease = vi.fn(async () => { + throw new Error("remote release failed"); + }); + const provider = createProductionSetupTokenSandboxProvider({ + environments: { + getById: async () => ({ id: "env-1", driver: "sandbox", status: "active" }) as never, + getLeaseById: async () => ({ id: "lease-orphan", environmentId: "env-1" }) as never, + releaseLease: async () => ({}) as never, + }, + environmentRuntime: { + acquireRunLease: vi.fn(), + getDriver: () => ({ releaseRunLease }) as never, + }, + openLivePtySession: async () => { + throw new Error("must not open a pty for a restart release"); + }, + }); + const store = createRecordingStore(); + const { service } = buildService({ sandbox: provider, store: store.store }); + + // A crash left a durable record whose lease the reaper must free. The + // in-memory lease map is empty, as after a restart. + const orphan: SetupTokenCleanupRecord = { + sessionId: "orphan-session", + companyId: SCOPE.companyId, + ownerUserId: SCOPE.ownerUserId, + adapterType: SCOPE.adapterType, + environmentId: SCOPE.environmentId, + leaseId: "lease-orphan", + deadline: 0, + state: "failed", + boundAt: null, + }; + store.rows.set(orphan.sessionId, { ...orphan }); + store.setReapable([orphan]); + + const result = await service.reap(Date.now()); + + // The remote release ran and failed, so the reaper counts a failure and keeps + // the durable cleanup record for a later retry. + expect(releaseRunLease).toHaveBeenCalledTimes(1); + expect(result).toEqual({ released: 0, failed: 1 }); + expect(store.removed).not.toContain("orphan-session"); + expect(store.rows.has("orphan-session")).toBe(true); + }); + + it("releases a leftover lease by id on a restart reap", async () => { + const sandbox = createFakeSandbox(); + const store = createRecordingStore(); + const { service } = buildService({ sandbox: sandbox.provider, store: store.store }); + + // A crash left a durable record whose lease the reaper must free after a + // restart. No live session references it. + const orphan: SetupTokenCleanupRecord = { + sessionId: "orphan-session", + companyId: SCOPE.companyId, + ownerUserId: SCOPE.ownerUserId, + adapterType: SCOPE.adapterType, + environmentId: SCOPE.environmentId, + leaseId: "lease-orphan", + deadline: 0, + state: "failed", + boundAt: null, + }; + store.setReapable([orphan]); + + const result = await service.reap(Date.now()); + + expect(result).toEqual({ released: 1, failed: 0 }); + expect(sandbox.releasedLeaseIds).toEqual(["lease-orphan"]); + expect(store.removed).toContain("orphan-session"); + }); +}); + +describe("production sandbox provider", () => { + it("fails closed before it acquires a lease when the live opener is not bound", async () => { + const acquireRunLease = vi.fn(); + const provider = createProductionSetupTokenSandboxProvider({ + environments: { + getById: async () => ({ id: "env-1", driver: "sandbox", status: "active" }) as never, + getLeaseById: async () => null, + releaseLease: async () => ({}) as never, + }, + environmentRuntime: { + acquireRunLease, + getDriver: () => null, + }, + // No `openLivePtySession`: the live path lands with the + // characterization test. + }); + + await expect(provider.acquire({ scope: SCOPE, deadline: Date.now() + 1000 })).rejects.toMatchObject({ + status: 503, + }); + // The provider held no lease, so it never acquired one. + expect(acquireRunLease).not.toHaveBeenCalled(); + }); + + it("fails closed for a local environment", async () => { + const provider = createProductionSetupTokenSandboxProvider({ + environments: { + getById: async () => ({ id: "env-1", driver: "local", status: "active" }) as never, + getLeaseById: async () => null, + releaseLease: async () => ({}) as never, + }, + environmentRuntime: { + acquireRunLease: vi.fn(), + getDriver: () => null, + }, + openLivePtySession: async () => { + throw new Error("must not reach the opener for a local environment"); + }, + }); + + await expect(provider.acquire({ scope: SCOPE, deadline: Date.now() + 1000 })).rejects.toMatchObject({ + status: 503, + }); + }); + + it("acquires a lease and binds the live opener when it is present", async () => { + const openPtySession: SetupTokenPtySessionOpener = async () => ({ + onData(): void {}, + write(): void {}, + wait: async () => ({ exitCode: 0 }), + kill(): void {}, + async close(): Promise {}, + }); + const releaseRunLease = vi.fn(async () => null); + const deadline = Date.now() + 1000; + const provider = createProductionSetupTokenSandboxProvider({ + environments: { + getById: async () => ({ id: "env-1", name: "Sandbox", driver: "sandbox", status: "active" }) as never, + getLeaseById: async () => null, + releaseLease: async () => ({}) as never, + }, + environmentRuntime: { + acquireRunLease: async () => + ({ + environment: { id: "env-1", driver: "sandbox" }, + // The runtime bounds the lease expiry to the session deadline. + lease: { id: "lease-live", expiresAt: new Date(deadline - 1) }, + leaseContext: {}, + }) as never, + getDriver: () => ({ releaseRunLease }) as never, + }, + openLivePtySession: async () => openPtySession, + }); + + const acquired = await provider.acquire({ scope: SCOPE, deadline }); + expect(acquired.leaseId).toBe("lease-live"); + + await provider.release(acquired.leaseId); + expect(releaseRunLease).toHaveBeenCalledTimes(1); + }); + + it("forwards the session deadline and records a lease expiry at or before it", async () => { + const openPtySession: SetupTokenPtySessionOpener = async () => ({ + onData(): void {}, + write(): void {}, + wait: async () => ({ exitCode: 0 }), + kill(): void {}, + async close(): Promise {}, + }); + const releaseRunLease = vi.fn(async () => null); + const deadline = Date.now() + 60_000; + let forwardedExpiresAt: Date | null | undefined; + const provider = createProductionSetupTokenSandboxProvider({ + environments: { + getById: async () => ({ id: "env-1", name: "Sandbox", driver: "sandbox", status: "active" }) as never, + getLeaseById: async () => null, + releaseLease: async () => ({}) as never, + }, + environmentRuntime: { + // The runtime records the provider-attested expiry. Here the provider + // grants an expiry at or before the requested deadline. + acquireRunLease: async (input: { requestedExpiresAt?: Date | null }) => { + forwardedExpiresAt = input.requestedExpiresAt; + const bounded = new Date(Math.min(deadline, deadline + 5_000)); + return { + environment: { id: "env-1", driver: "sandbox" }, + lease: { id: "lease-live", expiresAt: bounded }, + leaseContext: {}, + } as never; + }, + getDriver: () => ({ releaseRunLease }) as never, + }, + openLivePtySession: async () => openPtySession, + }); + + const acquired = await provider.acquire({ scope: SCOPE, deadline }); + + // The provider forwarded the session deadline to the runtime acquire. + expect(forwardedExpiresAt).toBeInstanceOf(Date); + expect((forwardedExpiresAt as Date).getTime()).toBe(deadline); + expect(acquired.leaseId).toBe("lease-live"); + // The lease held no expiry after the deadline. + expect(releaseRunLease).not.toHaveBeenCalled(); + }); + + it("releases the remote lease and fails closed when the acquired expiry is absent, invalid, or later", async () => { + const deadline = Date.now() + 60_000; + const cases: Array<{ label: string; expiresAt: unknown }> = [ + { label: "absent", expiresAt: null }, + { label: "invalid", expiresAt: new Date("not-a-date") }, + { label: "later", expiresAt: new Date(deadline + 1) }, + ]; + + for (const testCase of cases) { + const releaseRunLease = vi.fn(async () => null); + const openLivePtySession = vi.fn(async () => { + throw new Error("must not open a pty for an unbounded lease"); + }); + const provider = createProductionSetupTokenSandboxProvider({ + environments: { + getById: async () => ({ id: "env-1", name: "Sandbox", driver: "sandbox", status: "active" }) as never, + getLeaseById: async () => null, + releaseLease: async () => ({}) as never, + }, + environmentRuntime: { + acquireRunLease: async () => + ({ + environment: { id: "env-1", driver: "sandbox" }, + lease: { id: "lease-unbounded", expiresAt: testCase.expiresAt }, + leaseContext: {}, + }) as never, + getDriver: () => ({ releaseRunLease }) as never, + }, + openLivePtySession, + }); + + await expect(provider.acquire({ scope: SCOPE, deadline })).rejects.toMatchObject({ + status: 503, + }); + // The provider released the acquired remote lease through the driver. + expect(releaseRunLease, testCase.label).toHaveBeenCalledTimes(1); + // The provider failed closed before it opened the login pseudo-terminal. + expect(openLivePtySession, testCase.label).not.toHaveBeenCalled(); + } + }); + + it("runs the provider driver release by id for a fresh instance with an empty lease map", async () => { + // A fresh provider instance holds no in-memory lease record, as after a + // restart. The release must resolve the stored lease and its environment and + // run the provider driver release, not only the database row write. + const releaseRunLease = vi.fn(async () => null); + const releaseLease = vi.fn(async () => ({}) as never); + const getLeaseById = vi.fn(async () => ({ id: "lease-restart", environmentId: "env-1" }) as never); + const getById = vi.fn(async () => ({ id: "env-1", driver: "sandbox", status: "active" }) as never); + const provider = createProductionSetupTokenSandboxProvider({ + environments: { getById, getLeaseById, releaseLease }, + environmentRuntime: { + acquireRunLease: vi.fn(), + getDriver: () => ({ releaseRunLease }) as never, + }, + openLivePtySession: async () => { + throw new Error("must not open a pty for a restart release"); + }, + }); + + await provider.release("lease-restart"); + + // The provider resolved the lease by id and released through the driver. + expect(getLeaseById).toHaveBeenCalledWith("lease-restart"); + expect(releaseRunLease).toHaveBeenCalledTimes(1); + // The provider tore down the remote sandbox through the driver, not only the + // database lease row. + expect(releaseLease).not.toHaveBeenCalled(); + }); +}); + +describe("worker-bound live pseudo-terminal opener", () => { + it("resolves the lease to its provider lease id and drives the worker route gate", async () => { + const session = { + onData: () => {}, + write: () => {}, + wait: async () => ({ exitCode: 0 }), + kill: () => {}, + close: async () => {}, + }; + const openSetupTokenPtySession = vi.fn(async () => session); + const getLeaseById = vi.fn(async () => ({ + providerLeaseId: "provider-lease-9", + metadata: { pluginId: "paperclip.daytona", provider: "daytona" }, + })); + + const openLivePtySession = createWorkerBoundSetupTokenPtyOpener({ + workerManager: { openSetupTokenPtySession }, + environments: { getLeaseById }, + }); + + const opener = await openLivePtySession({ + scope: SCOPE, + environmentId: "env-1", + leaseId: "lease-42", + }); + // The opener resolved the server lease id to the provider lease id. + expect(getLeaseById).toHaveBeenCalledWith("lease-42"); + + const opened = await opener("claude setup-token"); + // The opener drove the manager route gate with the resolved worker target and + // the fixed command. The manager mints the host route id, so the opener passes + // none. + expect(openSetupTokenPtySession).toHaveBeenCalledWith("paperclip.daytona", { + driverKey: "daytona", + companyId: "company-1", + environmentId: "env-1", + providerLeaseId: "provider-lease-9", + command: "claude setup-token", + }); + expect(opened).toBe(session); + }); + + it("fails closed when the lease carries no sandbox worker binding", async () => { + const openSetupTokenPtySession = vi.fn(); + const openLivePtySession = createWorkerBoundSetupTokenPtyOpener({ + workerManager: { openSetupTokenPtySession }, + // The lease resolves but carries no provider lease id or plugin id. + environments: { + getLeaseById: async () => ({ providerLeaseId: null, metadata: {} }), + }, + }); + + await expect( + openLivePtySession({ scope: SCOPE, environmentId: "env-1", leaseId: "lease-42" }), + ).rejects.toMatchObject({ status: 503 }); + // The opener never reached the worker manager. + expect(openSetupTokenPtySession).not.toHaveBeenCalled(); + }); + + it("fails closed when the lease is missing", async () => { + const openLivePtySession = createWorkerBoundSetupTokenPtyOpener({ + workerManager: { openSetupTokenPtySession: vi.fn() }, + environments: { getLeaseById: async () => null }, + }); + + await expect( + openLivePtySession({ scope: SCOPE, environmentId: "env-1", leaseId: "missing" }), + ).rejects.toMatchObject({ status: 503 }); + }); +}); + +// The fixed, closed set of diagnostic lines the login runner emits. The runner +// is the sole producer. Every line is an approved non-secret literal. The +// application binding forwards each line verbatim, so a captured line must be a +// member of this set. +const DIAGNOSTIC_ALLOWLIST = new Set([ + "[paperclip] Setup-token login: the process stop step errored.", + "[paperclip] Setup-token login: the driver dispose step errored.", + "[paperclip] Setup-token login: the code input step errored.", + "[paperclip] Setup-token login: sent the browser code to the prompt.", + "[paperclip] Setup-token login: delivered the credential to the sink.", + "[paperclip] Setup-token login: the credential delivery step errored.", + "[paperclip] Setup-token login cancelled before start.", + "[paperclip] Setup-token login timed out; stopping the process.", + "[paperclip] Setup-token login cancelled; stopping the process.", + "[paperclip] Setup-token login command ended with a non-zero exit code.", + "[paperclip] Setup-token login: the credential did not land; treating the run as a failure.", + "[paperclip] Setup-token login: surfaced the sign-in prompt.", + "[paperclip] Setup-token login command ended successfully.", +]); + +// Synthetic sentinels. No real secret is present. The tests assert that no +// captured diagnostic line carries any of these values, so the binding never +// leaks the token, the browser code, or the authorization URL to a log. +const TOKEN_SENTINEL = "sk-ant-oat01-SENTINELTOKENAAAAAAAAAAAA"; +const BROWSER_CODE_SENTINEL = "SENTINEL-BROWSER-CODE"; +const URL_CLIENT_SENTINEL = "sentinelurlclient"; +// Each query value satisfies the shape contract the parser enforces: a 43-to-128 +// character `code_challenge`, a 16-to-256 character `state`, and the one pinned +// `redirect_uri`. The `client_id` stays the sentinel, so the leak checks still +// prove the client id never reaches a log line. +const AUTHORIZATION_URL_SENTINEL = + "https://claude.com/cai/oauth/authorize" + + `?client_id=${URL_CLIENT_SENTINEL}` + + "&code=redacted" + + "&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" + + "&code_challenge_method=S256" + + "&redirect_uri=https%3A%2F%2Fplatform.claude.com%2Foauth%2Fcode%2Fcallback" + + "&response_type=code" + + "&scope=user%3Ainference" + + "&state=Xy7Kd2Pq9Rn4Vb8Lf1Mw6Zc3Hj0Tg5Us"; + +// A complete sign-in prompt block. The parser binds the authorization URL to +// the preamble and the prompt line after it. +const PROMPT_OUTPUT = [ + "Browser didn’t open? Use the url below to sign in (c to copy)", + AUTHORIZATION_URL_SENTINEL, + "Paste code here if prompted >", +].join("\n"); + +// A complete success record. The parser binds the token between the two +// anchors. +const CREDENTIAL_OUTPUT = [ + "✓ Long-lived authentication token created successfully!", + "", + SETUP_TOKEN_BEFORE_ANCHOR, + "", + TOKEN_SENTINEL, + "", + SETUP_TOKEN_AFTER_ANCHOR, +].join("\n"); + +/** + * A fake sandbox that streams controllable output. The test emits the prompt + * block and the credential block on demand, and it ends the login process with + * a chosen exit code. The runner drives its full diagnostic path over this + * fake, so the test captures every line the binding forwards. + */ +function createStreamingSandbox() { + let listener: ((chunk: string) => void) | null = null; + let resolveExit: (value: { exitCode: number | null }) => void = () => {}; + const exit = new Promise<{ exitCode: number | null }>((resolve) => { + resolveExit = resolve; + }); + const writes: string[] = []; + + const openPtySession: SetupTokenPtySessionOpener = async () => ({ + onData(next): void { + listener = next; + }, + write(data): void { + writes.push(data); + }, + wait: () => exit, + kill(): void { + resolveExit({ exitCode: 137 }); + }, + async close(): Promise {}, + }); + + const provider: SetupTokenSandboxProvider = { + async acquire() { + return { leaseId: "lease-1", openPtySession }; + }, + async release() {}, + }; + + return { + provider, + emit(chunk: string): void { + listener?.(chunk); + }, + finishProcess(exitCode: number | null): void { + resolveExit({ exitCode }); + }, + get writes() { + return writes; + }, + }; +} + +/** Waits `ms` milliseconds of real time, so the code-submit settle can pass. */ +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Drives one login run through the binding factory and returns every captured + * diagnostic line. The caller controls the run through `drive`, so one call + * exercises the success path and another exercises the failure path. The `drive` + * callback receives the sandbox and the process handle. + */ +async function captureDiagnostics( + drive: ( + sandbox: ReturnType, + process: SetupTokenLoginProcess, + ) => Promise, +): Promise { + const captured: string[] = []; + const sandbox = createStreamingSandbox(); + const store = createRecordingStore(); + const transport = buildSetupTokenLoginTransport({ + sandbox: sandbox.provider, + store: store.store, + log: (line) => captured.push(line), + }); + + await transport.leases.acquire({ scope: SCOPE, deadline: Date.now() + 60_000 }); + const controller = new AbortController(); + const process = transport.factory({ + scope: SCOPE, + onPrompt: () => {}, + onCredential: async () => {}, + timeoutMs: 60_000, + signal: controller.signal, + }); + // Let the transport open the session and register the output listener before + // the driver emits any output. + await flush(); + + await drive(sandbox, process); + await process.done; + await flush(); + return captured; +} + +/** Asserts a captured diagnostic set is allowlisted and carries no secret. */ +function expectSafeDiagnostics(captured: string[]): void { + for (const line of captured) { + expect(DIAGNOSTIC_ALLOWLIST.has(line), line).toBe(true); + } + const joined = captured.join("\n"); + expect(joined).not.toContain(TOKEN_SENTINEL); + expect(joined).not.toContain(BROWSER_CODE_SENTINEL); + expect(joined).not.toContain(AUTHORIZATION_URL_SENTINEL); + expect(joined).not.toContain(URL_CLIENT_SENTINEL); +} + +describe("setup-token production transport binding diagnostics", () => { + it("forwards only allowlisted, non-secret diagnostics on a success path", async () => { + const captured = await captureDiagnostics(async (sandbox, process) => { + // Surface the prompt. The runner logs the prompt line and starts the code + // input step, which waits for the browser code. + sandbox.emit(PROMPT_OUTPUT); + await flush(); + // Submit the browser code. The runner writes the code, lets the paste + // buffer settle, writes the Enter byte, and logs the code-sent line. + process.submitCode(BROWSER_CODE_SENTINEL); + await delay(250); + // Deliver the token, then end the process cleanly. The runner binds the + // token, delivers it to the sink, and logs the success line. + sandbox.emit(CREDENTIAL_OUTPUT); + sandbox.finishProcess(0); + }); + + // The run reached the success lines, so the capture is not empty. + expect(captured).toContain("[paperclip] Setup-token login: surfaced the sign-in prompt."); + expect(captured).toContain( + "[paperclip] Setup-token login: delivered the credential to the sink.", + ); + expect(captured).toContain("[paperclip] Setup-token login command ended successfully."); + // The runner sent the browser code, so the code-input path ran. The code + // still never reaches a log line. + expect(captured).toContain("[paperclip] Setup-token login: sent the browser code to the prompt."); + expectSafeDiagnostics(captured); + }); + + it("forwards only allowlisted, non-secret diagnostics on a failure path", async () => { + const captured = await captureDiagnostics(async (sandbox, process) => { + void process; + // The login process ends with a non-zero exit code before any prompt. + sandbox.finishProcess(1); + }); + + expect(captured).toContain( + "[paperclip] Setup-token login command ended with a non-zero exit code.", + ); + expectSafeDiagnostics(captured); + }); +}); + +// A compile-time guard: a terminal state helper stays importable for the tests. +void isTerminalSessionState; + +// A synthetic token. No real token is present. The tests assert the token never +// reaches a log or an error message. +const SYNTH_TOKEN = "sk-ant-oat01-SYNTHETICSYNTHETICSYNTHETIC01"; + +describe("buildClaudeOAuthWriteInput", () => { + it("maps a scope with no overwrite to a first_write input", () => { + const input = buildClaudeOAuthWriteInput(SCOPE, "session-7", SYNTH_TOKEN); + expect(input).toEqual({ sessionId: "session-7", mode: "first_write", value: SYNTH_TOKEN }); + }); + + it("maps a scope with the overwrite capture to a confirmed_rotation input", () => { + const overwriteScope: SetupTokenSessionScope = { + ...SCOPE, + confirmedOverwrite: { expectedSecretId: "secret-9", expectedLatestVersion: 4 }, + }; + const input = buildClaudeOAuthWriteInput(overwriteScope, "session-8", SYNTH_TOKEN); + expect(input).toEqual({ + sessionId: "session-8", + mode: "confirmed_rotation", + value: SYNTH_TOKEN, + expectedSecretId: "secret-9", + expectedLatestVersion: 4, + }); + }); +}); + +describe("createSetupTokenSecretWriter wiring", () => { + it("carries the writer through the transport binding to the router", () => { + const sandbox = createFakeSandbox(); + const store = createRecordingStore(); + // The writer never runs here; a stub db is enough to assert the binding + // forwards the writer reference to the router. + const writer = createSetupTokenSecretWriter({ db: {} as never }); + + const transport = buildSetupTokenLoginTransport({ + sandbox: sandbox.provider, + store: store.store, + completeCredential: writer, + }); + + // The binding forwards the writer, so the router uses the real atomic write + // instead of the deferred fail-closed default. + expect(transport.completeCredential).toBe(writer); + }); +}); diff --git a/server/src/services/setup-token-transport-binding.ts b/server/src/services/setup-token-transport-binding.ts new file mode 100644 index 0000000000..4a74d1bcbe --- /dev/null +++ b/server/src/services/setup-token-transport-binding.ts @@ -0,0 +1,611 @@ +// The production transport binding for the Claude setup-token login session. +// +// The session service (`setup-token-session.ts`) drives the login flow over +// three injected components: a lease manager, a login-process factory, and a +// durable cleanup store. The router (`agents.ts`) accepts these components as an +// optional `setupTokenLogin` transport and falls back to a deferred, fail-closed +// default when a caller omits it. This module builds the production transport, so +// `app.ts` binds it one time at startup. +// +// The binding passes only the fixed command `CLAUDE_SETUP_TOKEN_COMMAND` to the +// login runner. It never accepts a command from a route, a request +// body, or an adapter configuration. +// +// The live pseudo-terminal opener is a separate seam. The Daytona sandbox +// provider runs as a plugin worker, so the server process does not hold the raw +// sandbox process. The real opener binds inside the worker; this module accepts +// it through `openLivePtySession`. A test injects a fake sandbox opener to drive +// the full session path. The live opener lands with the characterization +// test against a real sandbox. + +import { + SetupTokenSessionError, + SETUP_TOKEN_START_FAILED, + SETUP_TOKEN_STORAGE_FAILED, + createDbSetupTokenCleanupStore, + transitionSetupTokenSessionToStored, + type SetupTokenCleanupStore, + type SetupTokenLease, + type SetupTokenLeaseManager, + type SetupTokenLoginOutcome, + type SetupTokenLoginProcess, + type SetupTokenLoginProcessFactory, + type SetupTokenSecretWriter, + type SetupTokenSessionScope, +} from "./setup-token-session.js"; +import { secretService } from "./secrets.js"; +import type { environmentService } from "./environments.js"; +import type { environmentRuntimeService } from "./environment-runtime.js"; +import type { Db } from "@paperclipai/db"; +import { + createSetupTokenPtyTransport, + type SetupTokenPtySession, + type SetupTokenPtySessionOpener, +} from "@paperclipai/adapter-utils/setup-token-transport"; +import { + runSetupTokenLogin, + CLAUDE_SETUP_TOKEN_COMMAND, +} from "@paperclipai/adapter-claude-local/server"; + +/** + * The sandbox provider for one login session. It acquires a fresh sandbox lease + * and returns the lease id plus the pseudo-terminal opener bound to that lease's + * process. It releases the lease by id. The production provider wraps the + * environment runtime. A test injects a fake provider. + */ +export interface SetupTokenSandboxProvider { + /** + * Acquires a fresh sandbox lease for a login session. It returns the lease id + * and the pseudo-terminal opener bound to the acquired lease's process. It + * throws a fail-closed error when it cannot acquire the lease or bind the + * opener; the caller then returns the fixed start error. + */ + acquire(input: { + scope: SetupTokenSessionScope; + deadline: number; + }): Promise<{ leaseId: string; openPtySession: SetupTokenPtySessionOpener }>; + /** Releases the lease by id. The service and the startup reaper call it. */ + release(leaseId: string): Promise; +} + +/** The dependencies the production transport binding needs. */ +export interface SetupTokenLoginTransportDeps { + /** The sandbox provider. The production provider wraps the environment runtime. */ + sandbox: SetupTokenSandboxProvider; + /** The durable cleanup store. Defaults to the database-backed store over `db`. */ + store: SetupTokenCleanupStore; + /** + * The owner-bound secret writer. The binding forwards it to the router, so a + * completed login stores the minted token in the secret store. When a caller + * omits it, the router keeps its deferred, fail-closed writer. + */ + completeCredential?: SetupTokenSecretWriter; + /** A non-leaking status sink. It receives only fixed status lines. */ + log?: (line: string) => void; +} + +/** + * The production transport the router binds through `options.setupTokenLogin`. It + * carries the live lease manager, the login-process factory, the durable cleanup + * store, and the owner-bound secret writer. The router uses the writer for the + * final secret write; when the binding omits it, the router keeps its deferred, + * fail-closed writer. + */ +export interface SetupTokenLoginTransportBinding { + factory: SetupTokenLoginProcessFactory; + leases: SetupTokenLeaseManager; + store: SetupTokenCleanupStore; + completeCredential?: SetupTokenSecretWriter; +} + +/** + * The narrow secret-completion surface the production writer needs. The secrets + * service exposes `completeClaudeOAuthUserSecret` as the owner-bound + * compare-and-set for the Claude Code OAuth value. The writer forwards only the + * scope owner, the session id, the mode, and the token to it. + */ +export interface SetupTokenSecretCompletion { + completeClaudeOAuthUserSecret( + companyId: string, + ownerUserId: string, + input: { + sessionId: string; + mode: "first_write" | "confirmed_rotation"; + value: string; + expectedSecretId?: string | null; + expectedLatestVersion?: number | null; + }, + actor?: { userId?: string | null; agentId?: string | null }, + ): Promise; +} + +/** The owner-bound compare-and-set input the secrets service consumes. */ +export type ClaudeOAuthWriteInput = + | { sessionId: string; mode: "first_write"; value: string } + | { + sessionId: string; + mode: "confirmed_rotation"; + value: string; + expectedSecretId: string; + expectedLatestVersion: number; + }; + +/** + * Maps the session writer input to the secrets service compare-and-set input. The + * session scope selects the mode. A scope with no overwrite capture writes with + * `mode: "first_write"`, so the login creates the first owner value only. A scope + * that carries the confirmed-overwrite capture writes with + * `mode: "confirmed_rotation"`, so the login rotates the stored value under the + * captured `expectedSecretId` and `expectedLatestVersion`. The overwrite capture + * carries no owner; the owner still comes from the scope. + */ +export function buildClaudeOAuthWriteInput( + scope: SetupTokenSessionScope, + sessionId: string, + token: string, +): ClaudeOAuthWriteInput { + const overwrite = scope.confirmedOverwrite ?? null; + return overwrite + ? { + sessionId, + mode: "confirmed_rotation", + value: token, + expectedSecretId: overwrite.expectedSecretId, + expectedLatestVersion: overwrite.expectedLatestVersion, + } + : { sessionId, mode: "first_write", value: token }; +} + +/** + * Builds the production atomic credential-claim writer for the login session. It + * runs one control-plane transaction. Inside the transaction it first transitions + * the exact durable session row to `stored` with a full-scope conditional update, + * then writes or rotates the owner-bound secret on the same transaction handle. + * The commit establishes the encrypted secret and the `stored` claim together, so + * a crash between the two steps cannot leave a stored token with no valid claim. + * + * A zero-row transition means no claimable row exists in this scope. The writer + * rolls back and rejects with the fixed, non-secret storage error, so the session + * marks the login failed. A storage failure rejects and rolls back the whole + * transaction, so neither the secret nor the claim commits. A concurrent change + * fails the compare-and-set with the fixed stale conflict, so the write leaves no + * partial state. + * + * It reads the company and the owner only from the immutable session scope, so a + * request cannot redirect the write to another owner. It never logs + * the token and never puts the token in an error; it lets the secrets service + * error propagate unchanged, and the session maps a rejection to the fixed, + * non-secret storage error. + */ +export function createSetupTokenSecretWriter(deps: { + db: Db; + /** + * Builds the transaction-bound owner-bound completion. It defaults to + * `secretService(tx)`, so the compare-and-set runs on the passed transaction + * handle. A test injects a fake to force a storage failure after the transition. + */ + completionForTx?: (tx: Db) => SetupTokenSecretCompletion; +}): SetupTokenSecretWriter { + const completionForTx = deps.completionForTx ?? ((tx) => secretService(tx)); + return async ({ scope, sessionId, token }) => { + const input = buildClaudeOAuthWriteInput(scope, sessionId, token); + await deps.db.transaction(async (tx) => { + const txDb = tx as unknown as Db; + // Step 1: lock and conditionally transition the exact durable row to + // `stored`. The predicate matches the full scope, an allowed predecessor + // state, an unexpired deadline, and an unconsumed claim. It requires exactly + // one returned row. + const transitioned = await transitionSetupTokenSessionToStored(txDb, { + sessionId, + companyId: scope.companyId, + ownerUserId: scope.ownerUserId, + adapterType: scope.adapterType, + environmentId: scope.environmentId, + }); + if (!transitioned) { + // Zero-row result: roll back the whole transaction. The session then marks + // the login failed and returns the fixed no-secret error. + throw new SetupTokenSessionError(500, SETUP_TOKEN_STORAGE_FAILED); + } + // Step 2: write or rotate the secret on the same transaction handle. A + // failure here rolls back the transition too, so the commit is all-or-none. + await completionForTx(txDb).completeClaudeOAuthUserSecret( + scope.companyId, + scope.ownerUserId, + input, + { userId: scope.ownerUserId }, + ); + }); + }; +} + +/** The immutable scope key that correlates one acquire with one factory call. */ +function scopeKey(scope: SetupTokenSessionScope): string { + return [scope.companyId, scope.ownerUserId, scope.adapterType, scope.environmentId].join("\u0000"); +} + +/** + * Builds the production setup-token login transport. The lease manager acquires a + * sandbox lease through the injected provider and hands the pseudo-terminal + * opener to the factory. The factory drives the login runner over the transport + * and passes only the fixed command. The lease manager releases the lease on + * every terminal path, so the service runs its cleanup against the durable store. + */ +export function buildSetupTokenLoginTransport( + deps: SetupTokenLoginTransportDeps, +): SetupTokenLoginTransportBinding { + // The binding fixes the login command. It never reads a command from a route, a + // request body, an adapter configuration, or the dependency object, so a + // runtime-supplied value cannot change the pseudo-terminal command. + const command = CLAUDE_SETUP_TOKEN_COMMAND; + const log = deps.log ?? (() => {}); + + // The pseudo-terminal opener that one acquire hands to the next factory call. + // The service calls `leases.acquire` and then `factory` in sequence for one + // session, so a per-scope handoff correlates them. The per-owner session cap is + // one, so one scope holds at most one live acquire. + const pendingOpeners = new Map(); + // The reverse map releases a handoff by lease id when the factory never + // consumes it (a factory error path). + const leaseScopeKeys = new Map(); + + const leases: SetupTokenLeaseManager = { + async acquire({ scope, deadline }): Promise { + const acquired = await deps.sandbox.acquire({ scope, deadline }); + const key = scopeKey(scope); + pendingOpeners.set(key, acquired); + leaseScopeKeys.set(acquired.leaseId, key); + return { id: acquired.leaseId }; + }, + async release(lease): Promise { + dropHandoff(lease.id); + await deps.sandbox.release(lease.id); + }, + async releaseById(leaseId): Promise { + dropHandoff(leaseId); + await deps.sandbox.release(leaseId); + }, + }; + + function dropHandoff(leaseId: string): void { + const key = leaseScopeKeys.get(leaseId); + if (key !== undefined) { + const pending = pendingOpeners.get(key); + if (pending && pending.leaseId === leaseId) pendingOpeners.delete(key); + leaseScopeKeys.delete(leaseId); + } + } + + const factory: SetupTokenLoginProcessFactory = ({ scope, onPrompt, onCredential, timeoutMs, signal }): SetupTokenLoginProcess => { + const key = scopeKey(scope); + const pending = pendingOpeners.get(key); + if (!pending) { + // The lease manager did not hand off an opener for this scope. Fail closed. + throw new SetupTokenSessionError(503, SETUP_TOKEN_START_FAILED); + } + pendingOpeners.delete(key); + leaseScopeKeys.delete(pending.leaseId); + + const transport = createSetupTokenPtyTransport(pending.openPtySession); + + // Bridge the browser code. The service calls `submitCode` one time after the + // single `awaiting_code` transition wins. The runner calls `provideCode` one + // time after it surfaces the prompt. A resolved promise hands the code from + // the service call to the runner call. + let deliverCode: ((code: string) => void) | null = null; + const codeReady = new Promise((resolve, reject) => { + deliverCode = resolve; + // A cancel or an expiry aborts the signal. Reject the pending code, so the + // runner stops waiting and ends the run. + if (signal.aborted) { + reject(new SetupTokenSessionError(499, SETUP_TOKEN_START_FAILED)); + return; + } + signal.addEventListener( + "abort", + () => reject(new SetupTokenSessionError(499, SETUP_TOKEN_START_FAILED)), + { once: true }, + ); + }); + // The runner rejects the run when `provideCode` rejects. Consume the pending + // rejection here too, so it never becomes an unhandled rejection when the + // runner ends on the timeout or the signal before it reads the code. + codeReady.catch(() => {}); + + const done: Promise = runSetupTokenLogin(transport, { + command, + timeoutMs, + signal, + onPrompt: (prompt) => onPrompt({ url: prompt.url }), + provideCode: () => codeReady, + onCredential: async (authBytes) => { + // Forward the minted token to the owner-bound secret write. The service + // holds the token only for this call and never stores it. + await onCredential(authBytes.toString("utf8")); + }, + log, + }).then((result) => result.outcome); + + return { + done, + submitCode(code): void { + deliverCode?.(code); + }, + stop(): void { + // Stop the direct child. The service releases the lease after this call. + transport.stop(); + }, + }; + }; + + return { factory, leases, store: deps.store, completeCredential: deps.completeCredential }; +} + +/** The dependencies the production sandbox provider needs. */ +export interface ProductionSetupTokenSandboxProviderDeps { + environments: Pick, "getById" | "getLeaseById" | "releaseLease">; + environmentRuntime: Pick, "acquireRunLease" | "getDriver">; + /** + * Opens the live pseudo-terminal for the acquired lease. The Daytona provider + * runs as a plugin worker, so the real opener binds `sandbox.process` inside + * the worker. When a caller omits it, the provider fails closed before it + * acquires a lease, and the live path lands with the characterization + * test. + */ + openLivePtySession?: (input: { + scope: SetupTokenSessionScope; + environmentId: string; + leaseId: string; + }) => Promise; + /** A non-leaking status sink. It receives only fixed status lines. */ + log?: (line: string) => void; +} + +/** + * Builds the production sandbox provider over the environment runtime. It + * acquires a fresh sandbox lease for one login session, binds the live + * pseudo-terminal opener to that lease, and releases the lease by id. It fails + * closed for a local environment, an SSH environment, or a missing live opener. + */ +export function createProductionSetupTokenSandboxProvider( + deps: ProductionSetupTokenSandboxProviderDeps, +): SetupTokenSandboxProvider { + const log = deps.log ?? (() => {}); + // The acquired lease records, keyed by lease id. The provider releases a lease + // through its driver. The startup reaper releases a lease by id after a + // restart, when this map is empty; the provider then falls back to a direct + // database release. + const leaseRecords = new Map< + string, + Awaited["acquireRunLease"]>> + >(); + + const failClosed = (): never => { + throw new SetupTokenSessionError(503, SETUP_TOKEN_START_FAILED); + }; + + return { + async acquire({ scope, deadline }) { + const environment = await deps.environments.getById(scope.environmentId); + if (!environment) { + log("[paperclip] Setup-token login: the selected environment is not found."); + return failClosed(); + } + if (environment.driver === "local" || environment.driver === "ssh") { + // The login pseudo-terminal needs a sandbox. A local or an SSH + // environment has no sandbox process to run the login command in. + log("[paperclip] Setup-token login: the selected environment has no sandbox."); + return failClosed(); + } + if (!deps.openLivePtySession) { + // The live sandbox pseudo-terminal opener is not bound yet. Fail closed + // before the acquire, so the login holds no lease. The live opener lands + // with the characterization test against a real sandbox. + log( + "[paperclip] Setup-token login: the live sandbox pseudo-terminal transport is not bound.", + ); + return failClosed(); + } + + const leaseRecord = await deps.environmentRuntime.acquireRunLease({ + companyId: scope.companyId, + environment, + issueId: null, + agentId: scope.targetAgentId ?? null, + // A null heartbeat run and a null execution workspace disable lease + // reuse, so the login session always runs in a fresh sandbox. + heartbeatRunId: null, + persistedExecutionWorkspace: null, + adapterType: scope.adapterType, + // Apply the active custom-image template, so the sandbox binds to the + // trusted image and runtime identity. + applyCustomImageTemplate: true, + // Re-check the environment company binding inside the lease insert + // transaction. The route guard ran earlier, so a managed reconciliation + // can bind this sandbox to another company between the guard and this + // acquire. The lease insert then rejects a foreign-company environment + // with the 403 `environment_company_mismatch` and holds no lease. + assertCompanyBinding: true, + // Bound the lease expiry to the session deadline. The runtime records + // the earlier of this deadline and the provider expiry on the lease row. + requestedExpiresAt: new Date(deadline), + }); + const leaseId = leaseRecord.lease.id; + leaseRecords.set(leaseId, leaseRecord); + + // Enforce an independent, provider-backed expiry at or before the session + // deadline. A crash or an outage stops the in-process cleanup, so the + // lease row must carry a durable expiry that a lease reaper acts on. When + // the acquired lease has no expiry, an invalid expiry, or an expiry after + // the deadline, the server cannot guarantee the hard stop. Release the + // remote lease and fail closed, so the login holds no lease and leaves no + // durable cleanup row. + const expiresAt = leaseRecord.lease.expiresAt; + const expiresAtMs = expiresAt instanceof Date ? expiresAt.getTime() : Number.NaN; + if (!Number.isFinite(expiresAtMs) || expiresAtMs > deadline) { + await this.release(leaseId); + log("[paperclip] Setup-token login: the acquired lease expiry does not bound the session deadline."); + return failClosed(); + } + + try { + const openPtySession = await deps.openLivePtySession({ + scope, + environmentId: scope.environmentId, + leaseId, + }); + return { leaseId, openPtySession }; + } catch (err) { + // The opener bind failed. Release the lease and fail closed, so the login + // holds no lease. + await this.release(leaseId); + log("[paperclip] Setup-token login: the live pseudo-terminal bind failed."); + void err; + return failClosed(); + } + }, + + async release(leaseId) { + const leaseRecord = leaseRecords.get(leaseId); + if (leaseRecord) { + leaseRecords.delete(leaseId); + const driver = deps.environmentRuntime.getDriver(leaseRecord.environment.driver); + if (driver) { + await driver.releaseRunLease({ + environment: leaseRecord.environment, + lease: leaseRecord.lease, + status: "released", + }); + return; + } + } + // A restart cleared the in-memory record. Release the lease through the + // provider driver, not only the database row, so the release tears down the + // remote sandbox that holds the login state. + await releaseLeaseById(leaseId); + }, + }; + + /** + * Releases a lease by id after a restart, when the in-memory record is gone. It + * resolves the stored lease and its environment, then releases the lease through + * the provider driver, so the release tears down the remote sandbox and not only + * the database row. It fails loud when it resolves a live lease that it cannot + * release, so the reaper keeps the durable cleanup record and retries it. It + * returns without an error only when the lease row is already gone, because then + * no sandbox remains to release. + */ + async function releaseLeaseById(leaseId: string): Promise { + const lease = await deps.environments.getLeaseById(leaseId); + if (!lease) { + // The lease row is already gone. No remote sandbox remains to release, so + // the release is idempotent and the reaper may drop the cleanup record. + return; + } + const environment = lease.environmentId + ? await deps.environments.getById(lease.environmentId) + : null; + if (!environment) { + // The lease resolves but its environment is gone, so no driver can tear down + // the remote sandbox. Fail loud, so the reaper keeps the cleanup record. + log("[paperclip] Setup-token login: the lease environment is not found for a restart release."); + return failClosed(); + } + const driver = deps.environmentRuntime.getDriver(environment.driver); + if (!driver) { + // No driver resolves the remote sandbox. Fail loud, so the reaper keeps the + // cleanup record and retries the release. + log("[paperclip] Setup-token login: no driver resolves the lease for a restart release."); + return failClosed(); + } + await driver.releaseRunLease({ environment, lease, status: "released" }); + } +} + +/** Builds the durable, database-backed cleanup store for the production binding. */ +export function createProductionSetupTokenCleanupStore(db: Db): SetupTokenCleanupStore { + return createDbSetupTokenCleanupStore(db); +} + +/** + * The narrow plugin worker manager surface the live opener needs. The manager + * owns the host route gate: it mints the host route identifier, + * reserves one route per worker, drives the open, binds the worker session + * identifier one time, routes output, and terminalizes the route. + */ +export interface SetupTokenPtyWorkerManagerLike { + openSetupTokenPtySession( + pluginId: string, + input: { + driverKey: string; + companyId: string; + environmentId: string; + providerLeaseId: string; + command: string; + }, + ): Promise; +} + +/** The narrow lease-lookup surface the live opener needs. */ +export interface SetupTokenPtyLeaseLookup { + getLeaseById(leaseId: string): Promise< + | { + providerLeaseId: string | null; + metadata: Record | null | undefined; + } + | null + >; +} + +/** The dependencies the worker-bound live pseudo-terminal opener needs. */ +export interface WorkerBoundSetupTokenPtyOpenerDeps { + /** The plugin worker manager that owns the host route gate. */ + workerManager: SetupTokenPtyWorkerManagerLike; + /** The environment lease lookup that resolves the worker target for a lease. */ + environments: SetupTokenPtyLeaseLookup; + /** A non-leaking status sink. It receives only fixed status lines. */ + log?: (line: string) => void; +} + +function readLeaseMetaString(value: unknown): string | null { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +/** + * Builds the production `openLivePtySession`. It resolves the server lease to its + * `providerLeaseId` and the sandbox worker plugin id, then returns an opener that + * drives the worker through the manager route gate. The manager mints the host + * route identifier and owns the route lifecycle. The opener fails closed when the + * lease carries no sandbox worker binding. + */ +export function createWorkerBoundSetupTokenPtyOpener( + deps: WorkerBoundSetupTokenPtyOpenerDeps, +): NonNullable { + const log = deps.log ?? (() => {}); + return async ({ scope, environmentId, leaseId }) => { + const lease = await deps.environments.getLeaseById(leaseId); + const providerLeaseId = + typeof lease?.providerLeaseId === "string" && lease.providerLeaseId.length > 0 + ? lease.providerLeaseId + : null; + const metadata = + lease && typeof lease.metadata === "object" && lease.metadata !== null + ? (lease.metadata as Record) + : {}; + const pluginId = readLeaseMetaString(metadata.pluginId); + const driverKey = + readLeaseMetaString(metadata.provider) ?? readLeaseMetaString(metadata.driver); + if (!providerLeaseId || !pluginId || !driverKey) { + log("[paperclip] Setup-token login: the lease carries no sandbox worker binding."); + throw new SetupTokenSessionError(503, SETUP_TOKEN_START_FAILED); + } + return (command: string) => + deps.workerManager.openSetupTokenPtySession(pluginId, { + driverKey, + companyId: scope.companyId, + environmentId, + providerLeaseId, + command, + }); + }; +} diff --git a/server/src/shutdown.test.ts b/server/src/shutdown.test.ts index 74bf4b0fc7..5b8a3c7a95 100644 --- a/server/src/shutdown.test.ts +++ b/server/src/shutdown.test.ts @@ -2,9 +2,140 @@ import { EventEmitter } from "node:events"; import { describe, expect, it, vi } from "vitest"; import { coordinateHeartbeatSchedulerShutdown, + finalizeServerShutdown, loadWithoutCoordinatedShutdownSignalHooks, } from "./shutdown.js"; +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +function stubLogger() { + return { info: vi.fn(), error: vi.fn() }; +} + +describe("finalizeServerShutdown", () => { + it("awaits the setup-token cleanup before the database stop and the process exit", async () => { + const order: string[] = []; + // The held promise models the setup-token session cancellation and its + // sandbox lease release. The teardown must not continue while it is pending. + const release = deferred(); + const shutdownAppServices = vi.fn(async () => { + order.push("appServices:start"); + await release.promise; + order.push("appServices:settled"); + }); + const stopEmbeddedPostgres = vi.fn(async () => { + order.push("postgres:stop"); + }); + const shutdownInstrumentation = vi.fn(async () => { + order.push("instrumentation:flush"); + }); + + let exited = false; + const finalize = finalizeServerShutdown({ + signal: "SIGTERM", + shutdownAppServices, + stopEmbeddedPostgres, + shutdownInstrumentation, + log: stubLogger(), + }).then(() => { + // This models the caller's `process.exit(0)` continuation. + exited = true; + order.push("exit"); + }); + + // The cleanup is in flight. The database stop, the instrumentation flush, + // and the process exit continuation must all wait for it to settle. + await vi.waitFor(() => expect(shutdownAppServices).toHaveBeenCalledOnce()); + expect(stopEmbeddedPostgres).not.toHaveBeenCalled(); + expect(shutdownInstrumentation).not.toHaveBeenCalled(); + expect(exited).toBe(false); + + release.resolve(); + await finalize; + + expect(exited).toBe(true); + expect(order).toEqual([ + "appServices:start", + "appServices:settled", + "postgres:stop", + "instrumentation:flush", + "exit", + ]); + }); + + it("keeps the teardown durable and still exits when the setup-token release fails", async () => { + const order: string[] = []; + // The held promise rejects, which models a lease release that failed. The + // reaper owns the durable retry, so the teardown must log the failure and + // continue rather than swallow it or block the exit. + const release = deferred(); + const releaseError = new Error("lease release failed"); + const shutdownAppServices = vi.fn(async () => { + await release.promise; + }); + const stopEmbeddedPostgres = vi.fn(async () => { + order.push("postgres:stop"); + }); + const shutdownInstrumentation = vi.fn(async () => { + order.push("instrumentation:flush"); + }); + const log = stubLogger(); + + let exited = false; + const finalize = finalizeServerShutdown({ + signal: "SIGTERM", + shutdownAppServices, + stopEmbeddedPostgres, + shutdownInstrumentation, + log, + }).then(() => { + exited = true; + }); + + await vi.waitFor(() => expect(shutdownAppServices).toHaveBeenCalledOnce()); + expect(stopEmbeddedPostgres).not.toHaveBeenCalled(); + expect(exited).toBe(false); + + release.reject(releaseError); + await finalize; + + // The teardown surfaced the failure in the log, then finished the ordered + // teardown and reached the exit continuation. + expect(log.error).toHaveBeenCalledWith( + expect.objectContaining({ err: releaseError, signal: "SIGTERM" }), + expect.any(String), + ); + expect(order).toEqual(["postgres:stop", "instrumentation:flush"]); + expect(exited).toBe(true); + }); + + it("skips the database stop when no embedded PostgreSQL runs in this process", async () => { + const shutdownAppServices = vi.fn(async () => undefined); + const shutdownInstrumentation = vi.fn(async () => undefined); + const log = stubLogger(); + + await finalizeServerShutdown({ + signal: "SIGINT", + shutdownAppServices, + stopEmbeddedPostgres: null, + shutdownInstrumentation, + log, + }); + + expect(shutdownAppServices).toHaveBeenCalledOnce(); + expect(shutdownInstrumentation).toHaveBeenCalledOnce(); + expect(log.info).not.toHaveBeenCalled(); + }); +}); + describe("loadWithoutCoordinatedShutdownSignalHooks", () => { it("removes the eager signal handlers from the real embedded-postgres import", async () => { const before = { diff --git a/server/src/shutdown.ts b/server/src/shutdown.ts index 3100e5312b..9882e256ec 100644 --- a/server/src/shutdown.ts +++ b/server/src/shutdown.ts @@ -2,6 +2,56 @@ type HotRestartShutdownPreparation = { skipDrain: boolean; }; +type ShutdownLogger = { + info(obj: object, msg: string): void; + error(obj: object, msg: string): void; +}; + +/** + * Runs the final, ordered teardown of the server. It awaits the application + * service cleanup first, so a live setup-token login session stops and releases + * its sandbox lease before the database and the provider stop. The caller runs + * `process.exit(0)` only after this helper resolves, so an orderly shutdown + * never leaves a sandbox lease or confidential login state alive past the + * process exit. + * + * A step that rejects does not stop the teardown. The helper logs the error and + * continues to the next step. A failed setup-token lease release stays a + * durable record for the startup reaper; the helper surfaces it in the log + * instead of blocking the exit path. + */ +export async function finalizeServerShutdown(input: { + signal: "SIGINT" | "SIGTERM"; + shutdownAppServices: (() => Promise) | undefined; + stopEmbeddedPostgres: (() => Promise) | null; + shutdownInstrumentation: () => Promise; + log: ShutdownLogger; +}): Promise { + const { signal } = input; + + // Await the application service cleanup, so a live setup-token login session + // releases its sandbox lease before the database and the provider stop. A + // rejected cleanup stays durable for the reaper; it does not block the exit. + try { + await input.shutdownAppServices?.(); + } catch (err) { + input.log.error({ err, signal }, "Application service shutdown failed"); + } + + if (input.stopEmbeddedPostgres) { + input.log.info({ signal }, "Stopping embedded PostgreSQL"); + try { + await input.stopEmbeddedPostgres(); + } catch (err) { + input.log.error({ err }, "Failed to stop embedded PostgreSQL cleanly"); + } + } + + // Flush buffered OTel spans before the process goes away; without this await + // the exporter's final batch is dropped on exit. + await input.shutdownInstrumentation(); +} + const COORDINATED_SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"] as const; type ShutdownSignalTarget = { diff --git a/ui/src/api/agents.ts b/ui/src/api/agents.ts index 5dc32696d4..7cc773a6eb 100644 --- a/ui/src/api/agents.ts +++ b/ui/src/api/agents.ts @@ -10,6 +10,13 @@ import type { AdapterEnvironmentTestResult, AdapterAuthSessionResponse, AdapterAuthSessionOwnerResponse, + ClaudeSetupTokenSessionResponse, + ClaudeSetupTokenSessionOwnerResponse, + ClaudeSetupTokenSessionPrompt, + ClaudeSetupTokenCompletionResponse, + ClaudeSetupTokenOverwrite, + ClaudeOAuthTokenStatusResponse, + SubmitBrowserCodeRequest, AgentKeyCreated, AgentRuntimeState, AgentTaskSession, @@ -257,6 +264,51 @@ export const agentsApi = { `/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/login-sessions/${encodeURIComponent(sessionId)}/cancel`, {}, ), + // The Claude submitted-browser-code login uses the company-and-environment + // setup-token routes. The route fixes the `claude_local` adapter. The start + // response carries the panel mode; the authorization URL rides only through the + // guarded prompt read. The completion response carries a non-secret + // `storedSessionId` claim and no token. + // Reads the stored Claude OAuth token status for the authenticated owner. A + // 200 carries only the secret id and the latest version; a 404 means the owner + // has no stored value (indistinguishable from a foreign value). The client + // applies the stored token first and captures the version for a later + // version-checked overwrite. + getClaudeOAuthTokenStatus: (companyId: string) => + api.get( + `/companies/${encodeURIComponent(companyId)}/claude-oauth-token-status`, + ), + startClaudeSetupTokenLogin: ( + companyId: string, + data: { environmentId: string; overwrite?: ClaudeSetupTokenOverwrite }, + ) => + api.post( + `/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions`, + { adapterType: "claude_local", ...data }, + ), + getClaudeSetupTokenLoginStatus: (companyId: string, sessionId: string) => + api.get( + `/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}`, + ), + getClaudeSetupTokenLoginPrompt: (companyId: string, sessionId: string) => + api.get( + `/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/prompt`, + ), + submitClaudeSetupTokenBrowserCode: (companyId: string, sessionId: string, browserCode: string) => + api.post( + `/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/code`, + { browserCode } satisfies SubmitBrowserCodeRequest, + ), + completeClaudeSetupTokenLogin: (companyId: string, sessionId: string) => + api.post( + `/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/completion`, + {}, + ), + cancelClaudeSetupTokenLogin: (companyId: string, sessionId: string) => + api.post( + `/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/cancel`, + {}, + ), availableSkills: () => api.get<{ skills: AvailableSkill[] }>("/skills/available"), }; diff --git a/ui/src/components/AgentConfigForm.render.test.tsx b/ui/src/components/AgentConfigForm.render.test.tsx index ba559b0a02..f612238f48 100644 --- a/ui/src/components/AgentConfigForm.render.test.tsx +++ b/ui/src/components/AgentConfigForm.render.test.tsx @@ -5,11 +5,14 @@ import { createRoot, type Root } from "react-dom/client"; import { flushSync } from "react-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { Agent, Environment } from "@paperclipai/shared"; +import type { Agent, Environment, UserSecretDefinition } from "@paperclipai/shared"; +import { getEnvironmentCapabilities } from "@paperclipai/shared"; import { TooltipProvider } from "@/components/ui/tooltip"; import { ToastProvider } from "../context/ToastContext"; import { AgentConfigForm, AdapterLoginPanel, type AdapterLoginDescriptor } from "./AgentConfigForm"; import { defaultCreateValues } from "./agent-config-defaults"; +import { buildNewAgentHirePayload } from "../lib/new-agent-hire-payload"; +import { ApiError } from "../api/client"; const mockAgentsApi = vi.hoisted(() => ({ adapterModelProfiles: vi.fn(), @@ -20,6 +23,13 @@ const mockAgentsApi = vi.hoisted(() => ({ startAdapterAuthLogin: vi.fn(), getAdapterAuthLoginStatus: vi.fn(), cancelAdapterAuthLogin: vi.fn(), + startClaudeSetupTokenLogin: vi.fn(), + getClaudeSetupTokenLoginStatus: vi.fn(), + getClaudeSetupTokenLoginPrompt: vi.fn(), + submitClaudeSetupTokenBrowserCode: vi.fn(), + completeClaudeSetupTokenLogin: vi.fn(), + cancelClaudeSetupTokenLogin: vi.fn(), + getClaudeOAuthTokenStatus: vi.fn(), })); const mockClipboard = vi.hoisted(() => ({ @@ -28,6 +38,7 @@ const mockClipboard = vi.hoisted(() => ({ const mockEnvironmentsApi = vi.hoisted(() => ({ list: vi.fn(), + capabilities: vi.fn(), })); const mockInstanceSettingsApi = vi.hoisted(() => ({ @@ -39,6 +50,7 @@ const mockInstanceSettingsApi = vi.hoisted(() => ({ const mockSecretsApi = vi.hoisted(() => ({ list: vi.fn(), listProposals: vi.fn(), + listUserSecretDefinitions: vi.fn(async () => [] as unknown[]), })); vi.mock("../api/agents", () => ({ @@ -306,6 +318,34 @@ const AUTH_MISSING_RESULT = { testedAt: new Date(0).toISOString(), }; +const CLAUDE_AUTH_MISSING_RESULT = { + adapterType: "claude_local", + status: "warn", + checks: [ + { + code: "claude_hello_probe_auth_required", + level: "warn", + message: "Claude CLI is installed, but login is required.", + }, + { + code: "adapter_auth_missing", + level: "warn", + message: "The sandbox has no ready authentication for this adapter.", + }, + ], + testedAt: new Date(0).toISOString(), +}; + +// The provider capabilities the form fetches. Daytona advertises the +// setup-token login capability; E2B does not. The Claude login panel shows only +// for a provider with the capability. +const SANDBOX_CAPABILITIES = getEnvironmentCapabilities(["claude_local", "codex_local"], { + sandboxProviders: { + daytona: { supportsSetupTokenLogin: true, displayName: "Daytona" }, + e2b: { supportsSetupTokenLogin: false, displayName: "E2B" }, + }, +}); + function findButton(container: HTMLElement, label: string) { return Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.trim() === label, @@ -332,6 +372,107 @@ async function renderCodexSandbox(agentOverrides: Partial = {}) { ); } +async function renderClaudeSandbox(agentOverrides: Partial = {}) { + return renderForm( + [ + makeEnvironment({ id: "local-1", name: "Local", driver: "local" }), + makeEnvironment({ + id: "sandbox-1", + name: "Daytona", + driver: "sandbox", + config: { provider: "daytona" }, + }), + ], + { adapterType: "claude_local", defaultEnvironmentId: "sandbox-1", ...agentOverrides }, + { showAdapterTestEnvironmentButton: true }, + ); +} + +async function renderCreateClaudeSandbox( + valueOverrides: Partial = {}, +) { + return renderCreateForm( + [ + makeEnvironment({ id: "local-1", name: "Local", driver: "local" }), + makeEnvironment({ + id: "sandbox-1", + name: "Daytona", + driver: "sandbox", + config: { provider: "daytona" }, + }), + ], + { adapterType: "claude_local", defaultEnvironmentId: "sandbox-1", ...valueOverrides }, + { showAdapterTestEnvironmentButton: true }, + ); +} + +// A create-mode harness that holds the form values in React state. A value +// patch from the form (a login claim, an environment change) updates the props, +// so the form re-runs its effects against the new state. The fixed-values +// `renderCreateForm` harness cannot show the environment-change reset, because +// its `values` prop never changes. `valuesRef` exposes the current merged +// values to the test. +async function renderStatefulCreateClaudeSandbox(environments: Environment[]) { + mockEnvironmentsApi.list.mockResolvedValue(environments); + + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + + const valuesRef: { current: typeof defaultCreateValues } = { + current: { + ...defaultCreateValues, + adapterType: "claude_local", + defaultEnvironmentId: "sandbox-1", + }, + }; + + function Harness() { + const [values, setValues] = useState(valuesRef.current); + valuesRef.current = values; + return ( + setValues((prev) => ({ ...prev, ...patch }))} + hidePromptTemplate + showAdapterTypeField={false} + showAdapterTestEnvironmentButton + /> + ); + } + + await act(async () => { + root.render( + + + + + + + , + ); + }); + + await flushReact(); + return { container, root, valuesRef }; +} + +async function selectEnvironment(container: HTMLElement, environmentId: string) { + const select = container.querySelector("select"); + await act(async () => { + if (select) { + const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")?.set; + setter?.call(select, environmentId); + select.dispatchEvent(new Event("change", { bubbles: true })); + } + }); + await flushReact(); +} + async function clickByText(container: HTMLElement, label: string) { const button = findButton(container, label); await act(async () => { @@ -356,6 +497,19 @@ async function startLogin(container: HTMLElement) { await flushReact(); } +// Flush React effects and pending promises until a condition holds. The Claude +// login chains a start, a status poll, and a completion read, so a single flush +// does not settle every state transition. +async function flushUntil(check: () => boolean, timeoutMs = 4000) { + const start = Date.now(); + while (!check()) { + if (Date.now() - start > timeoutMs) break; + await flushReact(); + await new Promise((resolve) => setTimeout(resolve, 20)); + } + await flushReact(); +} + describe("AgentConfigForm environment selector", () => { let roots: Root[] = []; @@ -373,6 +527,7 @@ describe("AgentConfigForm environment selector", () => { mockInstanceSettingsApi.get.mockResolvedValue({ defaultEnvironmentId: null }); mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableEnvironments: true }); mockInstanceSettingsApi.getGeneral.mockResolvedValue({ executionMode: "any" }); + mockEnvironmentsApi.capabilities.mockResolvedValue(SANDBOX_CAPABILITIES); mockSecretsApi.list.mockResolvedValue([]); mockSecretsApi.listProposals.mockResolvedValue([]); mockAgentsApi.startAdapterAuthLogin.mockResolvedValue({ @@ -399,6 +554,39 @@ describe("AgentConfigForm environment selector", () => { prompt: null, }); mockClipboard.copyTextToClipboard.mockResolvedValue(undefined); + mockAgentsApi.startClaudeSetupTokenLogin.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "starting", + expiresAt: null, + failure: null, + panelMode: "submitted_browser_code", + prompt: null, + }); + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "waiting_for_user", + expiresAt: null, + failure: null, + }); + mockAgentsApi.getClaudeSetupTokenLoginPrompt.mockResolvedValue({ + authorizationUrl: "https://claude.example.test/authorize", + }); + mockAgentsApi.submitClaudeSetupTokenBrowserCode.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "authenticated", + expiresAt: null, + failure: null, + }); + mockAgentsApi.completeClaudeSetupTokenLogin.mockResolvedValue({ + storedSessionId: "stored-session-1", + }); + mockAgentsApi.cancelClaudeSetupTokenLogin.mockResolvedValue(undefined); + // Default: the owner has no stored Claude login. A test that needs a stored + // value overrides this with a status body. + mockAgentsApi.getClaudeOAuthTokenStatus.mockResolvedValue(null); }); afterEach(async () => { @@ -747,6 +935,77 @@ describe("AgentConfigForm environment selector", () => { expect(findButton(result.container, "Log in")).toBeTruthy(); }); + it("hides the Login button before Test and shows it after the adapter_auth_missing check for a Claude sandbox", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + expect(findButton(result.container, "Log in")).toBeFalsy(); + + await runTest(result.container); + + expect(findButton(result.container, "Log in")).toBeTruthy(); + }); + + it("hides the Login button for a Claude sandbox whose provider lacks the setup-token login capability", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + const result = await renderForm( + [ + makeEnvironment({ id: "local-1", name: "Local", driver: "local" }), + makeEnvironment({ + id: "sandbox-1", + name: "E2B", + driver: "sandbox", + config: { provider: "e2b" }, + }), + ], + { adapterType: "claude_local", defaultEnvironmentId: "sandbox-1" }, + { showAdapterTestEnvironmentButton: true }, + ); + roots.push(result.root); + + await runTest(result.container); + + // E2B does not advertise the setup-token login capability, so the panel + // stays hidden even after the auth-missing check. + expect(findButton(result.container, "Log in")).toBeFalsy(); + }); + + it("hides the Login button for a Daytona sandbox while the capabilities report no setup-token support", async () => { + // Reproduces the reported defect: the Test carries the auth-missing check, + // but the capabilities endpoint reports `supportsSetupTokenLogin: false` + // for Daytona (a stale persisted plugin manifest). The gate hides the + // panel. The server-side fix refreshes the persisted manifest so the + // capability reports true and the panel shows (see the companion positive + // test above). + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + mockEnvironmentsApi.capabilities.mockResolvedValue( + getEnvironmentCapabilities(["claude_local", "codex_local"], { + sandboxProviders: { + daytona: { supportsSetupTokenLogin: false, displayName: "Daytona" }, + }, + }), + ); + const result = await renderForm( + [ + makeEnvironment({ id: "local-1", name: "Local", driver: "local" }), + makeEnvironment({ + id: "sandbox-1", + name: "Daytona", + driver: "sandbox", + config: { provider: "daytona" }, + }), + ], + { adapterType: "claude_local", defaultEnvironmentId: "sandbox-1" }, + { showAdapterTestEnvironmentButton: true }, + ); + roots.push(result.root); + + await runTest(result.container); + + expect(findButton(result.container, "Log in")).toBeFalsy(); + }); + it("shows the Login button when a parent lifts the test feedback and renders the panel from the descriptor", async () => { // The create page hides the inline feedback branch and renders the test // result and the login panel itself. This harness mirrors that parent: it @@ -1079,4 +1338,1074 @@ describe("AgentConfigForm environment selector", () => { expect(findButton(result.container, "Log in")).toBeFalsy(); }); + + it("shows the authorization URL and a browser-code input for a Claude sandbox", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + (result.container.textContent ?? "").includes("https://claude.example.test/authorize"), + ); + + expect(mockAgentsApi.startClaudeSetupTokenLogin).toHaveBeenCalledWith("company-1", { + environmentId: "sandbox-1", + }); + // The panel shows the authorization URL and a browser-code input. It never + // shows a server-displayed code. + expect(result.container.textContent).toContain("https://claude.example.test/authorize"); + expect( + result.container.querySelector('input[aria-label="Browser code"]'), + ).toBeTruthy(); + // The default prompt carries no advisory: a confidential transport. So the + // panel shows no disclaimer. + expect(result.container.textContent).not.toContain("not encrypted"); + }); + + it("shows a non-blocking disclaimer when the transport advisory is present", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + // The guarded prompt read reports a non-confidential transport. The server + // does not block the login; it attaches the advisory. The panel shows a + // disclaimer and the login still proceeds. + mockAgentsApi.getClaudeSetupTokenLoginPrompt.mockResolvedValue({ + authorizationUrl: "https://claude.example.test/authorize", + transportAdvisory: { code: "insecure_transport" }, + }); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => (result.container.textContent ?? "").includes("not encrypted")); + + // The disclaimer states the transport risk in plain language. + expect(result.container.textContent).toContain("not encrypted"); + expect(result.container.textContent).toContain("clear text"); + // The login still proceeds: the panel shows the URL and the browser-code + // input, so the disclaimer never blocks the flow. + expect(result.container.textContent).toContain("https://claude.example.test/authorize"); + expect( + result.container.querySelector('input[aria-label="Browser code"]'), + ).toBeTruthy(); + }); + + it("submits one browser code and clears the input after submit", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + Boolean(result.container.querySelector('input[aria-label="Browser code"]')), + ); + + const input = result.container.querySelector( + 'input[aria-label="Browser code"]', + ); + setInputValue(input!, "BROWSERCODE-9"); + await flushReact(); + await clickByText(result.container, "Submit"); + await flushReact(); + + expect(mockAgentsApi.submitClaudeSetupTokenBrowserCode).toHaveBeenCalledWith( + "company-1", + "claude-session-1", + "BROWSERCODE-9", + ); + // The panel clears the browser code after submit, so the secret does not + // linger in the input. + const clearedInput = result.container.querySelector( + 'input[aria-label="Browser code"]', + ); + expect(clearedInput?.value).toBe(""); + }); + + it("treats the server stored state as success and captures the stored-session claim", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "authenticated", + expiresAt: null, + failure: null, + }); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => (result.container.textContent ?? "").includes("Authenticated")); + + expect(mockAgentsApi.completeClaudeSetupTokenLogin).toHaveBeenCalledWith( + "company-1", + "claude-session-1", + ); + expect(result.container.textContent).toContain("Authenticated"); + }); + + it("clears the stored-session claim and the fixed binding when the effective environment changes", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "authenticated", + expiresAt: null, + failure: null, + }); + const result = await renderStatefulCreateClaudeSandbox([ + makeEnvironment({ id: "local-1", name: "Local", driver: "local" }), + makeEnvironment({ + id: "sandbox-1", + name: "Daytona One", + driver: "sandbox", + config: { provider: "daytona" }, + }), + makeEnvironment({ + id: "sandbox-2", + name: "Daytona Two", + driver: "sandbox", + config: { provider: "daytona" }, + }), + ]); + roots.push(result.root); + + // Log in on the first sandbox. The stored state adds the fixed + // `CLAUDE_CODE_OAUTH_TOKEN` binding and the non-secret claim to the form. + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => result.valuesRef.current.claudeStoredSessionId != null); + + expect(result.valuesRef.current.claudeStoredSessionId).toBe("stored-session-1"); + expect("CLAUDE_CODE_OAUTH_TOKEN" in (result.valuesRef.current.envBindings ?? {})).toBe(true); + + // Change the effective environment. The reset drops the claim and the + // binding, so the create request cannot send a claim for the old target. + await selectEnvironment(result.container, "sandbox-2"); + + const values = result.valuesRef.current; + expect(values.claudeStoredSessionId ?? null).toBeNull(); + expect(values.claudeApplyStoredLogin ?? false).toBe(false); + expect("CLAUDE_CODE_OAUTH_TOKEN" in (values.envBindings ?? {})).toBe(false); + + // The create request body carries no stale claim. + const payload = buildNewAgentHirePayload({ + name: "Cody", + effectiveRole: "Engineer", + configValues: values, + adapterConfig: {}, + }); + expect("storedSessionId" in payload).toBe(false); + }); + + it("clears the false missing-definition error on the bound row after a login stores the token", async () => { + // Regression: the user-secret-definitions list read at page load does not + // yet contain the Claude token key, but it is not empty. A stale non-empty + // list makes the bound row show a false "no longer exists" error. The login + // must invalidate the list so the refetch clears the error. + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "authenticated", + expiresAt: null, + failure: null, + }); + const makeDefinition = (key: string): UserSecretDefinition => ({ + id: `def-${key}`, + companyId: "company-1", + key, + name: key.toUpperCase(), + description: null, + status: "active", + provider: "local_encrypted", + managedMode: "paperclip_managed", + providerConfigId: null, + providerMetadata: null, + usageGuidance: null, + createdByAgentId: null, + createdByUserId: null, + updatedByAgentId: null, + updatedByUserId: null, + deletedAt: null, + createdAt: new Date(0), + updatedAt: new Date(0), + }); + const staleList = [makeDefinition("OTHER_SECRET")]; + const freshList = [makeDefinition("OTHER_SECRET"), makeDefinition("CLAUDE_CODE_OAUTH_TOKEN")]; + mockSecretsApi.listUserSecretDefinitions.mockReset(); + mockSecretsApi.listUserSecretDefinitions + .mockResolvedValueOnce(staleList) + .mockResolvedValue(freshList); + + const result = await renderStatefulCreateClaudeSandbox([ + makeEnvironment({ id: "local-1", name: "Local", driver: "local" }), + makeEnvironment({ + id: "sandbox-1", + name: "Daytona", + driver: "sandbox", + config: { provider: "daytona" }, + }), + ]); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => result.valuesRef.current.claudeStoredSessionId != null); + + // The login bound the fixed Claude token row. + expect("CLAUDE_CODE_OAUTH_TOKEN" in (result.valuesRef.current.envBindings ?? {})).toBe(true); + + // The invalidation refetched the definitions, so the stale list no longer + // drives the row. + await flushUntil(() => mockSecretsApi.listUserSecretDefinitions.mock.calls.length >= 2); + await flushUntil(() => { + const inputs = Array.from(result.container.querySelectorAll("input")); + return inputs.some((input) => (input as HTMLInputElement).value === "CLAUDE_CODE_OAUTH_TOKEN"); + }); + + // Vacuous-pass guard: the bound row rendered. + const inputs = Array.from(result.container.querySelectorAll("input")); + expect(inputs.some((input) => (input as HTMLInputElement).value === "CLAUDE_CODE_OAUTH_TOKEN")).toBe(true); + // The row shows no false missing-definition error. + expect(result.container.textContent ?? "").not.toContain("no longer exists"); + }); + + it("reports the non-secret stored-session claim to the parent on success", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "authenticated", + expiresAt: null, + failure: null, + }); + const onStored = vi.fn(); + + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + roots.push(root); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + + await act(async () => { + root.render( + + + + + + + , + ); + }); + await startLogin(container); + await flushUntil(() => onStored.mock.calls.length > 0); + + expect(onStored).toHaveBeenCalledWith("stored-session-1"); + }); + + it("offers an apply-existing affordance when the status route reports a stored value", async () => { + mockAgentsApi.getClaudeOAuthTokenStatus.mockResolvedValue({ + secretId: "secret-1", + latestVersion: 3, + }); + const onApplyStored = vi.fn(); + + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + roots.push(root); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + + await act(async () => { + root.render( + + + + + + + , + ); + }); + await flushUntil(() => Boolean(findButton(container, "Use saved login"))); + + await clickByText(container, "Use saved login"); + + expect(onApplyStored).toHaveBeenCalledTimes(1); + // The panel shows the applied confirmation and hides the apply affordance. + await flushUntil(() => + (container.textContent ?? "").includes("The saved Claude login is bound to this agent now."), + ); + expect(findButton(container, "Use saved login")).toBeUndefined(); + // The apply-existing path never starts a login round trip. + expect(mockAgentsApi.startClaudeSetupTokenLogin).not.toHaveBeenCalled(); + }); + + it("does not offer the apply-existing affordance when there is no stored value", async () => { + mockAgentsApi.getClaudeOAuthTokenStatus.mockResolvedValue(null); + const onApplyStored = vi.fn(); + + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + roots.push(root); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + + await act(async () => { + root.render( + + + + + + + , + ); + }); + await flushUntil(() => Boolean(findButton(container, "Log in"))); + + expect(findButton(container, "Use saved login")).toBeUndefined(); + expect(onApplyStored).not.toHaveBeenCalled(); + }); + + it("returns to its start state with a fixed message on a server failed state", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "failed", + failure: { reason: "rejected", message: "the provider rejected the browser code" }, + expiresAt: null, + }); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + (result.container.textContent ?? "").includes("The login did not finish"), + ); + + // The panel shows a fixed message and returns to its start state. The Log in + // button is available again. + expect(result.container.textContent).toContain("The login did not finish"); + expect(findButton(result.container, "Log in")?.disabled).toBe(false); + // The panel never shows the provider failure message, which could carry a + // secret. + expect(result.container.textContent).not.toContain("the provider rejected the browser code"); + }); + + it("returns to its start state on a server timed_out state", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "timed_out", + failure: null, + expiresAt: null, + }); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + (result.container.textContent ?? "").includes("The login did not finish"), + ); + + expect(result.container.textContent).toContain("The login did not finish"); + expect(findButton(result.container, "Log in")?.disabled).toBe(false); + }); + + it("shows a terminal failure and stops polling on a status 404 from server cleanup", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + // The server removes the row and the in-memory session at once on a + // non-stored terminal state, so the status route returns 404 when the login + // fails and the cleanup wins the race against the next poll. The panel must + // fail loudly instead of holding stale data. + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockRejectedValue( + new ApiError("Setup-token login session not found.", 404, { + error: "Setup-token login session not found.", + }), + ); + // The prompt route also returns 404, so no authorization URL ever surfaces. + mockAgentsApi.getClaudeSetupTokenLoginPrompt.mockRejectedValue( + new ApiError("Setup-token login session not found.", 404, { + error: "Setup-token login session not found.", + }), + ); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + (result.container.textContent ?? "").includes("The login did not finish"), + ); + + // The panel shows the fixed failure message and returns to its start state. + expect(result.container.textContent).toContain("The login did not finish"); + expect(findButton(result.container, "Log in")?.disabled).toBe(false); + + // The panel shows no credential material: no authorization URL and no + // browser-code input. + expect(result.container.textContent).not.toContain("https://claude.example.test/authorize"); + expect(result.container.querySelector('input[aria-label="Browser code"]')).toBeFalsy(); + + // The stale polling stopped. The status call count stays fixed after the + // failure, so the panel does not poll a session the server removed. + const statusCallsAtFailure = mockAgentsApi.getClaudeSetupTokenLoginStatus.mock.calls.length; + await new Promise((resolve) => setTimeout(resolve, 2500)); + await flushReact(); + expect(mockAgentsApi.getClaudeSetupTokenLoginStatus.mock.calls.length).toBe( + statusCallsAtFailure, + ); + }); + + it("cancels an active Claude login and returns to its start state", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + (result.container.textContent ?? "").includes("https://claude.example.test/authorize"), + ); + + expect(findButton(result.container, "Cancel")).toBeTruthy(); + + await clickByText(result.container, "Cancel"); + await flushReact(); + + expect(mockAgentsApi.cancelClaudeSetupTokenLogin).toHaveBeenCalledWith( + "company-1", + "claude-session-1", + ); + // The panel resets: the Log in button is available again, and the URL and the + // browser-code input are gone. + expect(findButton(result.container, "Log in")?.disabled).toBe(false); + expect(findButton(result.container, "Cancel")).toBeFalsy(); + expect(result.container.textContent).not.toContain("https://claude.example.test/authorize"); + expect(result.container.querySelector('input[aria-label="Browser code"]')).toBeFalsy(); + }); + + it("treats a 404 cancel the same as success and returns to its start state", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + // The server removes a terminal session, so a cancel of an already-terminal + // or unknown session can return a 404. The panel must treat that 404 the + // same as a successful cancel: clear the session and stop the polls. + mockAgentsApi.cancelClaudeSetupTokenLogin.mockRejectedValue( + new ApiError("Setup-token login session not found.", 404, { + error: "Setup-token login session not found.", + }), + ); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + (result.container.textContent ?? "").includes("https://claude.example.test/authorize"), + ); + + await clickByText(result.container, "Cancel"); + await flushReact(); + + expect(mockAgentsApi.cancelClaudeSetupTokenLogin).toHaveBeenCalledWith( + "company-1", + "claude-session-1", + ); + // The panel reset even though the cancel returned a 404: the Log in button is + // available again, and the URL and the browser-code input are gone. No error + // message remains. + expect(findButton(result.container, "Log in")?.disabled).toBe(false); + expect(findButton(result.container, "Cancel")).toBeFalsy(); + expect(result.container.textContent).not.toContain("https://claude.example.test/authorize"); + expect(result.container.querySelector('input[aria-label="Browser code"]')).toBeFalsy(); + expect(result.container.textContent).not.toContain("Could not cancel the login."); + }); + + it("cancels the active server session when the panel unmounts", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + const result = await renderClaudeSandbox(); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + (result.container.textContent ?? "").includes("https://claude.example.test/authorize"), + ); + // The login is active before the unmount. + expect(findButton(result.container, "Cancel")).toBeTruthy(); + + await act(async () => { + result.root.unmount(); + }); + + // The unmount released the active server session, so the abandoned session + // does not hold the per-owner reservation until the server deadline. + expect(mockAgentsApi.cancelClaudeSetupTokenLogin).toHaveBeenCalledWith( + "company-1", + "claude-session-1", + ); + }); + + it("does not cancel on unmount when no login is active", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + const result = await renderClaudeSandbox(); + + await runTest(result.container); + // The panel shows the Log in button but no session started, so no active + // session exists to cancel. + expect(findButton(result.container, "Log in")).toBeTruthy(); + + await act(async () => { + result.root.unmount(); + }); + + expect(mockAgentsApi.cancelClaudeSetupTokenLogin).not.toHaveBeenCalled(); + }); + + it("stops both polls and shows the timed-out state at the server deadline", async () => { + vi.useFakeTimers(); + // Pin the clock to a known base. The panel arms the timed-out timer from the + // status `expiresAt`, so the test clock and the server deadline share one + // base time. + const baseNowMs = 1_700_000_000_000; + vi.setSystemTime(baseNowMs); + // The server deadline for this session. It is far longer than the old fixed + // 60-second cutoff, so the test proves the panel now tracks `expiresAt`. + const serverDeadlineMs = 5 * 60_000; + const expiresAtIso = new Date(baseNowMs + serverDeadlineMs).toISOString(); + try { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + // The status never reaches a terminal state, and the prompt route returns + // 404 forever, so the authorization URL never surfaces. The status route + // carries `expiresAt`, which drives the client cutoff. Without the client + // cap the panel would poll both routes forever. + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "waiting_for_user", + expiresAt: expiresAtIso, + failure: null, + }); + mockAgentsApi.getClaudeSetupTokenLoginPrompt.mockRejectedValue( + new ApiError("Setup-token login session not found.", 404, { + error: "Setup-token login session not found.", + }), + ); + + // Flush React effects and pending promises while the fake clock advances by + // zero, so the mocked queries settle without real time. + const flushFake = async () => { + await act(async () => { + for (let index = 0; index < 6; index += 1) { + await Promise.resolve(); + await vi.advanceTimersByTimeAsync(0); + } + }); + }; + const clickFake = async (container: HTMLElement, label: string) => { + const button = findButton(container, label); + await act(async () => { + button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushFake(); + }; + const advanceFake = async (ms: number) => { + await act(async () => { + await vi.advanceTimersByTimeAsync(ms); + }); + await flushFake(); + }; + + mockEnvironmentsApi.list.mockResolvedValue([ + makeEnvironment({ id: "local-1", name: "Local", driver: "local" }), + makeEnvironment({ + id: "sandbox-1", + name: "Daytona", + driver: "sandbox", + config: { provider: "daytona" }, + }), + ]); + + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + roots.push(root); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + await act(async () => { + root.render( + + + + + + + , + ); + }); + await flushFake(); + + await clickFake(container, "Test"); + await clickFake(container, "Log in"); + + // The login is active: both polls have run at least once. + const statusCallsAtStart = mockAgentsApi.getClaudeSetupTokenLoginStatus.mock.calls.length; + const promptCallsAtStart = mockAgentsApi.getClaudeSetupTokenLoginPrompt.mock.calls.length; + expect(statusCallsAtStart).toBeGreaterThanOrEqual(1); + expect(promptCallsAtStart).toBeGreaterThanOrEqual(1); + + // Advance six seconds: both polls run again, so polling is active. The panel + // has not timed out yet. + await advanceFake(6_000); + expect(mockAgentsApi.getClaudeSetupTokenLoginStatus.mock.calls.length).toBeGreaterThan( + statusCallsAtStart, + ); + expect(mockAgentsApi.getClaudeSetupTokenLoginPrompt.mock.calls.length).toBeGreaterThan( + promptCallsAtStart, + ); + expect(container.textContent).not.toContain("The login timed out"); + + // Advance past the old fixed sixty-second cutoff. The panel does NOT time + // out now, because the cutoff comes from the server `expiresAt`, which is + // far longer than sixty seconds. This proves the fixed 60-second cap is + // gone. + await advanceFake(60_000); + expect(container.textContent).not.toContain("The login timed out"); + + // Advance past the server deadline. The panel enters the timed-out state. + // Total elapsed after this step is 1 second past `serverDeadlineMs`. + await advanceFake(serverDeadlineMs - 66_000 + 1_000); + expect(container.textContent).toContain("The login timed out"); + // The panel released the server session when the cutoff fired. The cancel + // frees the per-owner reservation now, so an immediate retry by the same + // owner starts a new session and does not hit the "too many active + // sessions" cap. + expect(mockAgentsApi.cancelClaudeSetupTokenLogin).toHaveBeenCalledWith( + "company-1", + "claude-session-1", + ); + // The Log in button is available again, and the Cancel button is gone. + expect(findButton(container, "Log in")?.disabled).toBe(false); + expect(findButton(container, "Cancel")).toBeFalsy(); + + // Both polls stopped. A further ten seconds adds no new poll call. + const statusCallsAtTimeout = mockAgentsApi.getClaudeSetupTokenLoginStatus.mock.calls.length; + const promptCallsAtTimeout = mockAgentsApi.getClaudeSetupTokenLoginPrompt.mock.calls.length; + await advanceFake(10_000); + expect(mockAgentsApi.getClaudeSetupTokenLoginStatus.mock.calls.length).toBe( + statusCallsAtTimeout, + ); + expect(mockAgentsApi.getClaudeSetupTokenLoginPrompt.mock.calls.length).toBe( + promptCallsAtTimeout, + ); + } finally { + vi.useRealTimers(); + } + }); + + it("opens the authorization URL in a new tab with a safe rel", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + Boolean( + result.container.querySelector('a[href="https://claude.example.test/authorize"]'), + ), + ); + + const link = result.container.querySelector( + 'a[href="https://claude.example.test/authorize"]', + ); + expect(link).toBeTruthy(); + expect(link?.getAttribute("target")).toBe("_blank"); + expect(link?.getAttribute("rel")).toBe("noreferrer noopener"); + }); + + it("never renders the OAuth token in the Document Object Model", async () => { + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + // The mocks add a token field that the real response never carries. The + // panel must render neither the token nor any other unknown response field. + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "authenticated", + expiresAt: null, + failure: null, + oauthToken: "sk-ant-SECRET-TOKEN", + }); + mockAgentsApi.completeClaudeSetupTokenLogin.mockResolvedValue({ + storedSessionId: "stored-session-1", + oauthToken: "sk-ant-SECRET-TOKEN", + }); + const result = await renderClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => (result.container.textContent ?? "").includes("Authenticated")); + + expect(result.container.textContent).toContain("Authenticated"); + expect(result.container.textContent).not.toContain("sk-ant-SECRET-TOKEN"); + }); +}); + +const FIXED_CLAUDE_OAUTH_BINDING = { + type: "user_secret_ref", + key: "CLAUDE_CODE_OAUTH_TOKEN", + version: "latest", + required: true, +}; + +// Read the merged create-mode values after one or more onChange patches. The +// create form emits a partial patch, so later assertions merge every patch onto +// the seed values, the same way the parent page keeps the controlled state. +function mergedCreateValues( + seed: Record, + onChange: ReturnType, +): Record { + return onChange.mock.calls.reduce( + (acc, [patch]) => ({ ...acc, ...(patch as Record) }), + { ...seed }, + ); +} + +describe("AgentConfigForm create-mode Claude OAuth binding", () => { + let roots: Root[] = []; + + beforeEach(() => { + mockAgentsApi.adapterModelProfiles.mockResolvedValue([]); + mockAgentsApi.adapterModels.mockResolvedValue([]); + mockAgentsApi.detectModel.mockResolvedValue(null); + mockAgentsApi.list.mockResolvedValue([]); + mockInstanceSettingsApi.get.mockResolvedValue({ defaultEnvironmentId: null }); + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableEnvironments: true }); + mockInstanceSettingsApi.getGeneral.mockResolvedValue({ executionMode: "any" }); + mockEnvironmentsApi.capabilities.mockResolvedValue(SANDBOX_CAPABILITIES); + mockSecretsApi.list.mockResolvedValue([]); + mockSecretsApi.listProposals.mockResolvedValue([]); + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + mockAgentsApi.startClaudeSetupTokenLogin.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "starting", + expiresAt: null, + failure: null, + panelMode: "submitted_browser_code", + prompt: null, + }); + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "authenticated", + expiresAt: null, + failure: null, + }); + mockAgentsApi.getClaudeSetupTokenLoginPrompt.mockResolvedValue({ + authorizationUrl: "https://claude.example.test/authorize", + }); + mockAgentsApi.completeClaudeSetupTokenLogin.mockResolvedValue({ + storedSessionId: "stored-session-1", + }); + mockAgentsApi.cancelClaudeSetupTokenLogin.mockResolvedValue(undefined); + // Default: the owner has no stored Claude login. A test that needs a stored + // value overrides this with a status body. + mockAgentsApi.getClaudeOAuthTokenStatus.mockResolvedValue(null); + }); + + afterEach(async () => { + for (const root of roots) { + await act(async () => { + root.unmount(); + }); + } + roots = []; + document.body.innerHTML = ""; + vi.clearAllMocks(); + }); + + it("adds the fixed CLAUDE_CODE_OAUTH_TOKEN binding after the server stored state", async () => { + const result = await renderCreateClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + result.onChange.mock.calls.some( + ([patch]) => (patch as Record).claudeStoredSessionId, + ), + ); + + const values = mergedCreateValues( + { adapterType: "claude_local", defaultEnvironmentId: "sandbox-1", envBindings: {} }, + result.onChange, + ); + expect(values.claudeStoredSessionId).toBe("stored-session-1"); + const bindings = values.envBindings as Record; + expect(bindings.CLAUDE_CODE_OAUTH_TOKEN).toEqual(FIXED_CLAUDE_OAUTH_BINDING); + }); + + it("preserves unrelated environment bindings when it adds the fixed binding", async () => { + const seedBindings = { EXISTING_VAR: { type: "plain", value: "keep-me" } }; + const result = await renderCreateClaudeSandbox({ envBindings: seedBindings }); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + result.onChange.mock.calls.some( + ([patch]) => (patch as Record).claudeStoredSessionId, + ), + ); + + const values = mergedCreateValues( + { adapterType: "claude_local", envBindings: seedBindings }, + result.onChange, + ); + const bindings = values.envBindings as Record; + expect(bindings.EXISTING_VAR).toEqual({ type: "plain", value: "keep-me" }); + expect(bindings.CLAUDE_CODE_OAUTH_TOKEN).toEqual(FIXED_CLAUDE_OAUTH_BINDING); + }); + + it("never puts a token value in the fixed binding", async () => { + const result = await renderCreateClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + result.onChange.mock.calls.some( + ([patch]) => (patch as Record).claudeStoredSessionId, + ), + ); + + const values = mergedCreateValues( + { adapterType: "claude_local", envBindings: {} }, + result.onChange, + ); + const bindings = values.envBindings as Record>; + // The fixed binding is a reference. It carries no `value` field, so the + // adapter config never holds the token value. + expect(bindings.CLAUDE_CODE_OAUTH_TOKEN.type).toBe("user_secret_ref"); + expect(bindings.CLAUDE_CODE_OAUTH_TOKEN).not.toHaveProperty("value"); + expect(JSON.stringify(values.envBindings)).not.toContain("sk-ant"); + }); + + it("returns to the login start state when the server claim write fails", async () => { + mockAgentsApi.completeClaudeSetupTokenLogin.mockRejectedValue( + new Error("the provider rejected the stored-session claim"), + ); + const result = await renderCreateClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => + (result.container.textContent ?? "").includes("The login did not finish"), + ); + + // The panel shows a fixed, non-secret message and returns to its start state. + expect(result.container.textContent).toContain("The login did not finish"); + expect(findButton(result.container, "Log in")?.disabled).toBe(false); + expect(result.container.textContent).not.toContain( + "the provider rejected the stored-session claim", + ); + // A failed claim adds no binding and holds no claim. + expect( + result.onChange.mock.calls.some( + ([patch]) => (patch as Record).claudeStoredSessionId, + ), + ).toBe(false); + }); + +}); + +// Render the edit-mode form for an existing Claude agent in a sandbox +// environment. The helper returns the `onSave` spy so a test can read the patch +// that a stored login sends to the agent-update path. +async function renderEditClaudeSandbox(agentOverrides: Partial = {}) { + mockEnvironmentsApi.list.mockResolvedValue([ + makeEnvironment({ id: "local-1", name: "Local", driver: "local" }), + makeEnvironment({ + id: "sandbox-1", + name: "Daytona", + driver: "sandbox", + config: { provider: "daytona" }, + }), + ]); + + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + const onSave = vi.fn().mockResolvedValue(undefined); + + await act(async () => { + root.render( + + + + + + + , + ); + }); + + await flushReact(); + return { container, root, onSave }; +} + +describe("AgentConfigForm edit-mode Claude OAuth binding", () => { + let roots: Root[] = []; + + beforeEach(() => { + mockAgentsApi.adapterModelProfiles.mockResolvedValue([]); + mockAgentsApi.adapterModels.mockResolvedValue([]); + mockAgentsApi.detectModel.mockResolvedValue(null); + mockAgentsApi.list.mockResolvedValue([]); + mockInstanceSettingsApi.get.mockResolvedValue({ defaultEnvironmentId: null }); + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableEnvironments: true }); + mockInstanceSettingsApi.getGeneral.mockResolvedValue({ executionMode: "any" }); + mockEnvironmentsApi.capabilities.mockResolvedValue(SANDBOX_CAPABILITIES); + mockSecretsApi.list.mockResolvedValue([]); + mockSecretsApi.listProposals.mockResolvedValue([]); + mockAgentsApi.testEnvironment.mockResolvedValue(CLAUDE_AUTH_MISSING_RESULT); + mockAgentsApi.startClaudeSetupTokenLogin.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "starting", + expiresAt: null, + failure: null, + panelMode: "submitted_browser_code", + prompt: null, + }); + mockAgentsApi.getClaudeSetupTokenLoginStatus.mockResolvedValue({ + sessionId: "claude-session-1", + environmentId: "sandbox-1", + status: "authenticated", + expiresAt: null, + failure: null, + }); + mockAgentsApi.getClaudeSetupTokenLoginPrompt.mockResolvedValue({ + authorizationUrl: "https://claude.example.test/authorize", + }); + mockAgentsApi.completeClaudeSetupTokenLogin.mockResolvedValue({ + storedSessionId: "stored-session-1", + }); + mockAgentsApi.cancelClaudeSetupTokenLogin.mockResolvedValue(undefined); + // Default: the owner has no stored Claude login. A test that needs a stored + // value overrides this with a status body. + mockAgentsApi.getClaudeOAuthTokenStatus.mockResolvedValue(null); + }); + + afterEach(async () => { + for (const root of roots) { + await act(async () => { + root.unmount(); + }); + } + roots = []; + document.body.innerHTML = ""; + vi.clearAllMocks(); + }); + + it("adds the fixed CLAUDE_CODE_OAUTH_TOKEN binding to the agent and persists it", async () => { + const result = await renderEditClaudeSandbox(); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => result.onSave.mock.calls.length > 0); + + // A stored login sends one agent-update patch. The patch replaces the adapter + // config, and its env holds the fixed binding, so the agent keeps the binding + // without a manual step. + const patch = result.onSave.mock.calls.at(-1)![0] as Record; + expect(patch.replaceAdapterConfig).toBe(true); + const adapterConfig = patch.adapterConfig as Record; + const bindings = adapterConfig.env as Record; + expect(bindings.CLAUDE_CODE_OAUTH_TOKEN).toEqual(FIXED_CLAUDE_OAUTH_BINDING); + // The persisted patch never carries a token value. + expect(JSON.stringify(adapterConfig.env)).not.toContain("sk-ant"); + // An update can never consume the fresh login's stored-session claim -- only + // create and hire can, per `enforceClaudeOAuthBindingClaim`. So a save that + // introduces the binding through an update must set `applyStoredClaudeLogin`, + // or the server rejects the patch with the fixed claim error even though the + // login already stored the token. Regression coverage for that gap. + expect(patch.applyStoredClaudeLogin).toBe(true); + }); + + it("keeps every unrelated existing binding when it adds the fixed binding", async () => { + const result = await renderEditClaudeSandbox({ + adapterConfig: { env: { EXISTING_VAR: { type: "plain", value: "keep-me" } } }, + }); + roots.push(result.root); + + await runTest(result.container); + await startLogin(result.container); + await flushUntil(() => result.onSave.mock.calls.length > 0); + + const patch = result.onSave.mock.calls.at(-1)![0] as Record; + const adapterConfig = patch.adapterConfig as Record; + const bindings = adapterConfig.env as Record; + expect(bindings.EXISTING_VAR).toEqual({ type: "plain", value: "keep-me" }); + expect(bindings.CLAUDE_CODE_OAUTH_TOKEN).toEqual(FIXED_CLAUDE_OAUTH_BINDING); + }); + }); diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 3e5d771a7d..ff0a83f177 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -10,9 +10,10 @@ import type { EnvSecretRefBinding, Environment, } from "@paperclipai/shared"; -import { AGENT_DEFAULT_MAX_CONCURRENT_RUNS, supportedEnvironmentDriversForAdapter } from "@paperclipai/shared"; +import { AGENT_DEFAULT_MAX_CONCURRENT_RUNS, supportedEnvironmentDriversForAdapter, isValidBrowserCode } from "@paperclipai/shared"; import type { AdapterModel } from "../api/agents"; import { agentsApi } from "../api/agents"; +import { ApiError } from "../api/client"; import { environmentsApi } from "../api/environments"; import { instanceSettingsApi } from "../api/instanceSettings"; import { secretsApi } from "../api/secrets"; @@ -57,6 +58,7 @@ import { EnvironmentVariablesEditor, type EnvironmentVariablesEditorHandle, } from "./environment-variables-editor"; +import { buildFixedClaudeOAuthBinding, CLAUDE_OAUTH_TOKEN_ENV_KEY } from "./environment-variables-editor/model"; import { AgentSecretAccessEditor } from "./AgentSecretAccessEditor"; import { useProposalReview } from "../pages/secrets/proposal-review"; import { AGENT_ACCESS_CONFIG_PATH_PREFIX } from "../lib/secret-delivery"; @@ -444,6 +446,108 @@ export function AgentConfigForm(props: AgentConfigFormProps) { const set = isCreate ? (patch: Partial) => props.onChange(patch) : null; + + // Create mode holds the non-secret stored-session claim after a Claude + // subscription login reaches the server `stored` state. The claim marks the + // fixed `CLAUDE_CODE_OAUTH_TOKEN` binding as present. The form sends the claim + // in the create request; the server binds and enforces the token + // independently. The claim never carries a token value. + const claudeStoredSessionId = isCreate ? val!.claudeStoredSessionId ?? null : null; + + // After a login binds CLAUDE_CODE_OAUTH_TOKEN, the user-secret-definitions list + // in the cache is stale. The form reads that list once at page load, so it does + // not contain the definition the login just bound. The bound row then compares + // its key against the stale list and shows a false "no longer exists" health + // error. Invalidate the list so the row reads the fresh definitions and clears + // the error. + const invalidateUserSecretDefinitions = () => { + if (!selectedCompanyId) return; + void queryClient.invalidateQueries({ + queryKey: queryKeys.secrets.userDefinitions(selectedCompanyId), + }); + }; + + // Add the fixed binding to the create-mode form state after the login stores. + // Keep every unrelated binding. Hold the claim so the create request sends it. + const handleClaudeLoginStored = (storedSessionId: string) => { + if (!isCreate || !set) return; + const existingBindings = (val!.envBindings ?? {}) as Record; + set({ + envBindings: { ...existingBindings, ...buildFixedClaudeOAuthBinding() }, + claudeStoredSessionId: storedSessionId, + }); + invalidateUserSecretDefinitions(); + }; + + // Edit mode: after a login stores the token, add the fixed binding to the + // existing agent and persist it at once. The server already stored the token; + // this step binds `CLAUDE_CODE_OAUTH_TOKEN` to the agent's environment through + // the normal agent-update patch path, so no manual bind step remains. Flush any + // pending editor draft first, keep every unrelated binding, and mark the merged + // set into the overlay so the editor and the saved patch agree. + // + // An update can never consume a fresh login's stored-session claim -- only the + // create and hire paths can, per `enforceClaudeOAuthBindingClaim`. So this save + // must set `applyStoredClaudeLogin`, the same way `handleApplyStoredClaudeLoginEdit` + // does, or the server rejects the patch with the fixed claim error even though + // the login already stored the token. Without the flag every fresh login on an + // existing agent's own page fails to save the binding. + const handleClaudeLoginStoredEdit = async () => { + if (isCreate) return; + const flushedEnv = flushEnvironmentDraft(); + const baseEnv = + flushedEnv ?? + (eff("adapterConfig", "env", (config.env ?? EMPTY_ENV) as Record)); + const nextEnv = { ...baseEnv, ...buildFixedClaudeOAuthBinding() }; + const nextOverlay: AgentConfigOverlay = { + ...overlay, + adapterConfig: { ...overlay.adapterConfig, env: nextEnv }, + }; + setOverlay(nextOverlay); + await props.onSave({ + ...buildAgentUpdatePatch(props.agent, nextOverlay), + applyStoredClaudeLogin: true, + }); + invalidateUserSecretDefinitions(); + }; + + // Create mode: bind the fixed reference to an existing stored login with no new + // login round trip. Add the fixed binding and set the apply-existing flag. The + // create request sends the flag; the server binds the token only for a user + // actor and only when a stored value exists. Keep every unrelated binding. + const handleApplyStoredClaudeLogin = () => { + if (!isCreate || !set) return; + const existingBindings = (val!.envBindings ?? {}) as Record; + set({ + envBindings: { ...existingBindings, ...buildFixedClaudeOAuthBinding() }, + claudeApplyStoredLogin: true, + }); + invalidateUserSecretDefinitions(); + }; + + // Edit mode: bind the fixed reference to an existing stored login with no new + // login round trip, then persist it at once. Add the fixed binding to the + // existing agent and save the patch with the apply-existing flag. Flush any + // pending editor draft first and keep every unrelated binding. + const handleApplyStoredClaudeLoginEdit = async () => { + if (isCreate) return; + const flushedEnv = flushEnvironmentDraft(); + const baseEnv = + flushedEnv ?? + (eff("adapterConfig", "env", (config.env ?? EMPTY_ENV) as Record)); + const nextEnv = { ...baseEnv, ...buildFixedClaudeOAuthBinding() }; + const nextOverlay: AgentConfigOverlay = { + ...overlay, + adapterConfig: { ...overlay.adapterConfig, env: nextEnv }, + }; + setOverlay(nextOverlay); + await props.onSave({ + ...buildAgentUpdatePatch(props.agent, nextOverlay), + applyStoredClaudeLogin: true, + }); + invalidateUserSecretDefinitions(); + }; + const rawCurrentDefaultEnvironmentId = isCreate ? val!.defaultEnvironmentId ?? "" : eff("identity", "defaultEnvironmentId", props.agent.defaultEnvironmentId ?? ""); @@ -483,6 +587,18 @@ export function AgentConfigForm(props: AgentConfigFormProps) { () => environments.find((environment) => environment.id === effectiveLoginEnvironmentId) ?? null, [environments, effectiveLoginEnvironmentId], ); + // Load the sandbox provider capabilities. The Claude setup-token login runs on + // a real pseudo-terminal, so it needs a provider that advertises the + // setup-token login capability. The login panel gate reads this to hide the + // panel for a provider without the capability. The gate is advisory; the + // server resolves the capability again and fails closed. + const { data: environmentCapabilities } = useQuery({ + queryKey: selectedCompanyId + ? queryKeys.environments.capabilities(selectedCompanyId) + : ["environment-capabilities", "none"], + queryFn: () => environmentsApi.capabilities(selectedCompanyId!), + enabled: Boolean(selectedCompanyId) && adapterType === "claude_local", + }); // When the instance forces Kubernetes execution, new agents must default to the // managed Kubernetes sandbox environment (never the implicit local default). @@ -790,24 +906,68 @@ export function AgentConfigForm(props: AgentConfigFormProps) { // re-run on every render (the mutation object has a new identity each render). const resetTestEnvironmentRef = useRef(testEnvironment.reset); resetTestEnvironmentRef.current = testEnvironment.reset; + + // Clear a stale Claude login claim when the adapter type or the effective + // environment changes. A login binds `CLAUDE_CODE_OAUTH_TOKEN` to one specific + // environment. After the environment changes, the held claim and the fixed + // binding no longer match the new target. The create request would still send + // the claim, and the server would reject it. So drop the claim, the + // apply-existing flag, and the fixed binding, and return the login panel to + // its initial state. Only create mode holds this form state; edit mode saves + // the binding at login time. Hold the clear in a ref so the effect does not + // re-run on every render (`set` and `val` have a new identity each render). + const clearClaudeLoginClaimRef = useRef<() => void>(() => {}); + clearClaudeLoginClaimRef.current = () => { + if (!isCreate || !set) return; + const bindings = (val!.envBindings ?? {}) as Record; + const hasClaim = + val!.claudeStoredSessionId != null || val!.claudeApplyStoredLogin === true; + const hasBinding = CLAUDE_OAUTH_TOKEN_ENV_KEY in bindings; + if (!hasClaim && !hasBinding) return; + const nextBindings = { ...bindings }; + delete nextBindings[CLAUDE_OAUTH_TOKEN_ENV_KEY]; + set({ + envBindings: nextBindings, + claudeStoredSessionId: null, + claudeApplyStoredLogin: false, + }); + }; + useEffect(() => { resetTestEnvironmentRef.current(); setTestActionError(null); + clearClaudeLoginClaimRef.current(); }, [adapterType, effectiveLoginEnvironmentId]); - // Show the login affordance only for a current `codex_local` sandbox whose most - // recent Test result carries the canonical auth-missing check. The result keeps - // its own `adapterType`, so a result from another adapter never gates the panel. + // Show the login affordance only for a current `codex_local` or `claude_local` + // sandbox whose most recent Test result carries the canonical auth-missing + // check. Both adapters emit the same neutral code. The result keeps its own + // `adapterType`, so a result from another adapter never gates the panel. + const adapterSupportsSandboxLogin = + adapterType === "codex_local" || adapterType === "claude_local"; const authMissingCheck = - testEnvironment.data?.adapterType === "codex_local" + testEnvironment.data?.adapterType === "codex_local" || + testEnvironment.data?.adapterType === "claude_local" ? testEnvironment.data.checks.find((check) => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE) ?? null : null; + // The Claude login runs on a real pseudo-terminal, so it needs a provider that + // advertises the setup-token login capability. Read the capability for the + // effective environment provider. The codex login uses a different flow and + // does not gate on this capability, so the requirement applies to Claude only. + const effectiveLoginProvider = + typeof effectiveLoginEnvironment?.config?.provider === "string" + ? effectiveLoginEnvironment.config.provider + : null; + const providerSupportsSetupTokenLogin = + effectiveLoginProvider != null && + environmentCapabilities?.sandboxProviders?.[effectiveLoginProvider]?.supportsSetupTokenLogin === true; const showAdapterLogin = - adapterType === "codex_local" && + adapterSupportsSandboxLogin && effectiveLoginEnvironment?.driver === "sandbox" && Boolean(effectiveLoginEnvironmentId) && Boolean(selectedCompanyId) && - Boolean(authMissingCheck); + Boolean(authMissingCheck) && + (adapterType !== "claude_local" || providerSupportsSetupTokenLogin); const runEnvironmentTest = useCallback(async () => { if (!selectedCompanyId) { throw new Error("Select a company to test adapter environment"); @@ -1344,6 +1504,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) { companyId={selectedCompanyId!} adapterType={adapterType} environmentId={effectiveLoginEnvironmentId!} + onStored={isCreate ? handleClaudeLoginStored : handleClaudeLoginStoredEdit} + onApplyStored={ + isCreate ? handleApplyStoredClaudeLogin : handleApplyStoredClaudeLoginEdit + } /> )} @@ -1560,8 +1724,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { value={ isCreate ? ((val!.envBindings ?? EMPTY_ENV) as Record) - : ((eff("adapterConfig", "env", (config.env ?? EMPTY_ENV) as Record)) - ) + : (eff("adapterConfig", "env", (config.env ?? EMPTY_ENV) as Record)) } secrets={availableSecrets} userSecretDefinitions={userSecretDefinitions} @@ -1837,11 +2000,32 @@ export type AdapterLoginDescriptor = { environmentId: string; }; -export function AdapterLoginPanel({ +// The panel props. `onStored` reports the non-secret `storedSessionId` claim from +// a Claude login that reaches the server `stored` state. The create-mode form +// holds that claim for the fixed binding; the callback never carries a token. +// `onApplyStored` binds the fixed reference to an existing stored login with no +// new login round trip. The panel shows the apply-existing affordance only when +// the status route reports a stored value. +export type AdapterLoginPanelProps = AdapterLoginDescriptor & { + onStored?: (storedSessionId: string) => void; + onApplyStored?: () => void; +}; + +// The login panel dispatcher. It picks the panel mode from the adapter. The +// Claude adapter shows the submitted-browser-code panel. Every other adapter +// shows the displayed-code panel. +export function AdapterLoginPanel(props: AdapterLoginPanelProps) { + if (props.adapterType === "claude_local") { + return ; + } + return ; +} + +function DisplayedCodeLoginPanel({ companyId, adapterType, environmentId, -}: AdapterLoginDescriptor) { +}: AdapterLoginPanelProps) { const [sessionId, setSessionId] = useState(null); const [startError, setStartError] = useState(null); // The server delivers the one-time prompt on the first owner read only. Latch @@ -1996,6 +2180,546 @@ export function AdapterLoginPanel({ ); } +// The public session states that fail a Claude login. The panel returns to its +// start state and shows a fixed message at one of these. +const CLAUDE_LOGIN_FAILURE_STATUSES = new Set([ + "failed", + "timed_out", + "cancelled", +]); + +// The fixed, non-secret message for a failed Claude login. The panel shows this +// text and returns to its start state. It never shows a provider message that +// could carry a secret. +const CLAUDE_LOGIN_FAILED_MESSAGE = "The login did not finish. Start the login again."; + +// The client wall-clock cap for one active login. The panel polls the status +// route and the prompt route every two seconds. The server can leave a session +// short of a terminal status, and the prompt route returns 404 until the URL is +// ready. Without a cap, the panel polls forever. When this cap passes, the panel +// stops both polls and shows the timed-out state, so the user can start again. +// A generous client failsafe for a live login whose server deadline has not +// arrived yet. The server `expiresAt` is the real cutoff; the panel drives the +// timer from it. This failsafe only bounds a stuck poll while the status route +// has not yet returned `expiresAt`, so a login can never poll forever. The +// value is far longer than the server budget, so it never cuts a normal login +// short (the earlier fixed 60-second cutoff did, which drove this fix). +const CLAUDE_LOGIN_FAILSAFE_TIMEOUT_MS = 15 * 60_000; + +// The fixed, non-secret message for a timed-out Claude login. The panel shows +// this text, stops both polls, and returns to its start state. +const CLAUDE_LOGIN_TIMED_OUT_MESSAGE = "The login timed out. Start the login again."; + +// The submitted-browser-code login panel for the Claude adapter. It starts a +// setup-token login, polls the status route, and reads the authorization URL +// from the guarded prompt route. The user opens the URL, reads the browser code, +// and submits it. The panel clears the code right after submit. The panel treats +// only the server `stored` state as success, and it never shows the OAuth token. +function SubmittedBrowserCodeLoginPanel({ + companyId, + environmentId, + onStored, + onApplyStored, +}: AdapterLoginPanelProps) { + const [sessionId, setSessionId] = useState(null); + const [startError, setStartError] = useState(null); + // The server delivers the authorization URL on the guarded prompt read only. + // Latch it so a later poll does not hide the URL. + const [authorizationUrl, setAuthorizationUrl] = useState(null); + // True when the guarded prompt read reports a non-confidential transport. The + // server does not block a plain-HTTP login. The panel shows a non-blocking + // disclaimer and the login still proceeds. Latch it beside the URL. + const [transportInsecure, setTransportInsecure] = useState(false); + // The browser code the user submits. The panel clears it right after submit, so + // the secret never lingers in the input. + const [browserCode, setBrowserCode] = useState(""); + // The non-secret stored-session claim from a completed login. It marks the + // server `stored` state, the only success state. + const [storedSessionId, setStoredSessionId] = useState(null); + // True after a completion read fails. The panel returns to its start state. + const [completionFailed, setCompletionFailed] = useState(false); + // True after the client wall-clock cap passes for the active login. The panel + // stops both polls and shows the timed-out state. + const [timedOut, setTimedOut] = useState(false); + // True after the status poll returns 404. The server removes the row and the + // in-memory session at once on any non-stored terminal state, so a status 404 + // means the login failed and the server cleaned up. The panel stops both + // polls and shows a terminal failure state. It shows no credential material. + const [statusGone, setStatusGone] = useState(false); + // Guards the one completion read per session. + const completionStartedRef = useRef(false); + // True after the owner applies an existing stored login in this panel. The + // apply-existing path binds the fixed reference with no new login round trip, + // so the panel shows the applied confirmation and hides the apply affordance. + const [appliedStored, setAppliedStored] = useState(false); + + const resetLocalState = () => { + setStartError(null); + setAuthorizationUrl(null); + setTransportInsecure(false); + setBrowserCode(""); + setStoredSessionId(null); + setCompletionFailed(false); + setTimedOut(false); + setStatusGone(false); + completionStartedRef.current = false; + }; + + // Read the stored Claude OAuth token status when the panel mounts. A 200 body + // carries the secret id and the version of the owner value; a 404 means the + // owner has no stored value. The panel applies the stored token first: when a + // value exists, a replacement login rotates it under the captured version + // instead of a blind first write. The server rejects a stale capture, so a + // change by another process after the capture fails the overwrite. + const storedTokenQuery = useQuery({ + queryKey: ["claude-oauth-token-status", companyId], + queryFn: async () => { + try { + return await agentsApi.getClaudeOAuthTokenStatus(companyId); + } catch (error) { + // A 404 means the owner has no stored value (indistinguishable from a + // foreign value). The panel treats it as "no stored token". + if (error instanceof ApiError && error.status === 404) return null; + throw error; + } + }, + retry: (failureCount, error) => { + if (error instanceof ApiError && error.status === 404) return false; + return failureCount < 2; + }, + }); + const storedToken = storedTokenQuery.data ?? null; + + const startLogin = useMutation({ + mutationFn: () => + agentsApi.startClaudeSetupTokenLogin(companyId, { + environmentId, + // When the owner already has a stored token, the login rotates it under + // the captured version, so a replacement login never conflicts with an + // existing value. Without a stored token the login is a first write. + ...(storedToken + ? { + overwrite: { + expectedSecretId: storedToken.secretId, + expectedLatestVersion: storedToken.latestVersion, + }, + } + : {}), + }), + onSuccess: (session) => { + resetLocalState(); + setSessionId(session.sessionId); + }, + onError: (error) => { + setStartError(error instanceof Error ? error.message : "Could not start the login."); + }, + }); + + const clearActiveSession = () => { + // Return the panel to its idle start state. The Log in button is available + // again, and both polls stop because the session id is null. + setSessionId(null); + resetLocalState(); + }; + + const cancelLogin = useMutation({ + mutationFn: () => agentsApi.cancelClaudeSetupTokenLogin(companyId, sessionId!), + onSuccess: () => { + clearActiveSession(); + }, + onError: (error) => { + // The cancel is resilient. The server removes a terminal session, so a + // cancel of an already-terminal or unknown session can return a 404. Treat + // that 404 the same as a successful cancel: clear the session and stop the + // polls. The panel never keeps polling a session the server no longer + // holds. Any other error surfaces and keeps the active login. + if (error instanceof ApiError && error.status === 404) { + clearActiveSession(); + return; + } + setStartError(error instanceof Error ? error.message : "Could not cancel the login."); + }, + }); + + // Release the server session at once, without a change to the panel state. The + // client-cutoff timer and the unmount path both use this. The server holds a + // per-owner reservation until the session reaches a terminal state, so an + // abandoned session locks the owner out until the server deadline. A best- + // effort cancel frees that reservation now, so the same owner can start a new + // login and does not hit the "too many active sessions" cap. The server + // removes a terminal session, so a 404 means the session is already gone. Treat + // that 404 the same as a successful cancel. This is a fire-and-forget cleanup, + // so it drops every error. The manual Cancel button uses the `cancelLogin` + // mutation instead, because that path also returns the panel to its idle start + // state. + const releaseServerSession = useCallback( + (id: string) => { + void agentsApi.cancelClaudeSetupTokenLogin(companyId, id).catch(() => { + // Drop the error. A 404 means the server already removed the session. A + // cleanup path cannot surface any other error, so it stays silent. + }); + }, + [companyId], + ); + + // Both polls run only while a session is active and the client cap has not + // passed. The timeout stops the polls, so the panel never polls forever. A + // status 404 also stops the polls: the server cleaned up the session, so the + // panel enters a terminal failure state instead. + const pollingEnabled = Boolean(sessionId) && !timedOut && !statusGone; + + const statusQuery = useQuery({ + queryKey: ["claude-setup-token-status", companyId, sessionId], + queryFn: () => agentsApi.getClaudeSetupTokenLoginStatus(companyId, sessionId!), + enabled: pollingEnabled, + // A status 404 is terminal. The server removes a cleaned-up session at once, + // so a retry cannot recover it. Stop at once and fail loudly. + retry: (failureCount, error) => { + if (error instanceof ApiError && error.status === 404) return false; + return failureCount < 3; + }, + refetchInterval: (query) => { + if (timedOut || statusGone) return false; + const status = query.state.data?.status; + return status && ADAPTER_LOGIN_TERMINAL_STATUSES.has(status) + ? false + : ADAPTER_LOGIN_POLL_INTERVAL_MS; + }, + }); + + // Fail loudly on a status 404. The server cleans up the reservation, the row, + // and the in-memory session at once on any non-stored terminal state, so the + // status route returns 404 when a login fails and the server cleanup wins the + // race against the next poll. React Query keeps the last successful data on + // error, so without this branch the panel would hold stale data and show + // nothing. Enter the terminal failure state, which stops both polls. + useEffect(() => { + const error = statusQuery.error; + if (error instanceof ApiError && error.status === 404) { + setStatusGone(true); + } + }, [statusQuery.error]); + + // Poll the guarded prompt route until it returns the authorization URL. The + // route returns 404 until the URL is ready, so the panel treats a 404 as + // not-ready and keeps polling. + const promptQuery = useQuery({ + queryKey: ["claude-setup-token-prompt", companyId, sessionId], + enabled: pollingEnabled && !authorizationUrl, + queryFn: async () => { + try { + return await agentsApi.getClaudeSetupTokenLoginPrompt(companyId, sessionId!); + } catch (error) { + if (error instanceof ApiError && error.status === 404) return null; + throw error; + } + }, + refetchInterval: (query) => + timedOut || query.state.data?.authorizationUrl ? false : ADAPTER_LOGIN_POLL_INTERVAL_MS, + }); + + useEffect(() => { + const url = promptQuery.data?.authorizationUrl ?? null; + if (url) setAuthorizationUrl(url); + // The advisory rides the same guarded prompt read as the URL. Latch it, so a + // later poll does not clear the disclaimer. + if (promptQuery.data?.transportAdvisory) setTransportInsecure(true); + }, [promptQuery.data]); + + const submitCode = useMutation({ + mutationFn: (code: string) => + agentsApi.submitClaudeSetupTokenBrowserCode(companyId, sessionId!, code), + onError: (error) => { + setStartError(error instanceof Error ? error.message : "Could not submit the browser code."); + }, + }); + + const completeLogin = useMutation({ + mutationFn: () => agentsApi.completeClaudeSetupTokenLogin(companyId, sessionId!), + onSuccess: (result) => { + // The server reached the `stored` state. Hold the non-secret claim and + // report it to the parent. The response carries no token. + setStoredSessionId(result.storedSessionId); + onStored?.(result.storedSessionId); + }, + onError: () => { + setCompletionFailed(true); + }, + }); + + const status = statusQuery.data?.status ?? startLogin.data?.status ?? null; + // The server deadline for the active session (ISO string). The status route + // and the start response both carry it. The panel drives the client cutoff + // from this value, so the client and the server share one deadline. + const expiresAt = statusQuery.data?.expiresAt ?? startLogin.data?.expiresAt ?? null; + + // Complete the login once the server authenticates the session. The completion + // read returns the non-secret stored-session claim. Fire it once per session. + const completeLoginRef = useRef(completeLogin.mutate); + completeLoginRef.current = completeLogin.mutate; + useEffect(() => { + if (status === "authenticated" && !completionStartedRef.current) { + completionStartedRef.current = true; + completeLoginRef.current(); + } + }, [status]); + + const isStored = storedSessionId !== null; + const isFailure = + completionFailed || + statusGone || + Boolean(status && CLAUDE_LOGIN_FAILURE_STATUSES.has(status)); + const isCompleting = status === "authenticated" && !isStored && !completionFailed; + const isActive = Boolean(sessionId) && !isStored && !isFailure && !timedOut; + const startDisabled = startLogin.isPending || isActive; + + // Hold the active session id for the unmount cleanup. The panel updates it on + // every render. When the panel unmounts, or the parent removes it as the login + // closes, with an active, non-terminal session, the cleanup releases that + // session on the server. The ref is null once the session leaves the active + // state, so the cleanup never cancels a session the server already removed. + const activeSessionRef = useRef(null); + activeSessionRef.current = isActive ? sessionId : null; + + useEffect(() => { + return () => { + const id = activeSessionRef.current; + if (id) releaseServerSession(id); + }; + }, [releaseServerSession]); + + // Cap the active login at the server deadline. The timer arms when the login + // becomes active and clears when the login leaves the active state (a terminal + // status, a stored success, or a new login). It re-arms when `expiresAt` + // arrives or changes. When it fires, the panel enters the timed-out state. The + // `isActive` guard already excludes `timedOut`, so the timer does not re-arm + // after it fires. + // + // The delay comes from the server `expiresAt`, so the client cutoff matches + // the server session budget instead of a fixed short window. While `expiresAt` + // has not arrived for the live session, the panel uses a generous failsafe, so + // a stuck poll still stops. + useEffect(() => { + if (!isActive) return; + const deadlineMs = expiresAt !== null ? new Date(expiresAt).getTime() : Number.NaN; + const remainingMs = Number.isFinite(deadlineMs) + ? Math.max(0, deadlineMs - Date.now()) + : CLAUDE_LOGIN_FAILSAFE_TIMEOUT_MS; + const timer = setTimeout(() => { + // Release the server session first, then show the timed-out state. The + // cancel frees the server reservation now, so an immediate retry by the + // same owner starts a new session instead of a lockout on the per-owner + // cap. The panel keeps the timed-out state, so the user sees why the login + // stopped. + if (sessionId) releaseServerSession(sessionId); + setTimedOut(true); + }, remainingMs); + return () => clearTimeout(timer); + }, [isActive, expiresAt, sessionId, releaseServerSession]); + + const trimmedCode = browserCode.trim(); + const canSubmit = + isActive && + Boolean(authorizationUrl) && + !isCompleting && + isValidBrowserCode(trimmedCode) && + !submitCode.isPending; + + const handleSubmit = () => { + if (!canSubmit) return; + submitCode.mutate(trimmedCode); + // Clear the browser code right after submit, so the secret never lingers in + // the input. + setBrowserCode(""); + }; + + return ( +
+
+ Sign in to the sandbox +
+ {isActive && ( + + )} + {/* Apply the existing stored login with no new login round trip. The + affordance shows only when the status route reports a stored value + and no active or completed login runs in this panel. */} + {onApplyStored && storedToken && !isActive && !isStored && !appliedStored && ( + + )} + +
+
+ + {/* When the owner already has a stored Claude login, the panel applies it + first and does not force a fresh login. Use saved login binds the stored + token; a replacement login rotates it under the captured version. */} + {storedToken && !isActive && !isStored && !appliedStored && ( +
+ You have a saved Claude login. Use it to bind this agent, or log in again to replace the + stored token. +
+ )} + + {appliedStored && ( +
+ + The saved Claude login is bound to this agent now. +
+ )} + + {startError && ( +
+ {startError} +
+ )} + + {/* One live region announces the loading, prompt, completing, and terminal + states, so a screen reader reports each transition. */} +
+ {isActive && !authorizationUrl && !isCompleting && ( +
+ + Preparing the login… +
+ )} + + {isActive && authorizationUrl && !isCompleting && ( +
+ {transportInsecure && ( +
+ + + This connection is not encrypted. The login code travels in clear text on this + network. Continue only on a network you trust. + +
+ )} +
+ Open the authorization page, then enter the browser code it shows. +
+
+
+
+ Authorization URL +
+ {authorizationUrl} +
+
+ + +
+
+
+
+ Browser code +
+
+ setBrowserCode(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + handleSubmit(); + } + }} + className="flex-1 min-w-0 rounded-md border border-border bg-background px-2 py-1 font-mono text-xs text-foreground" + /> + +
+
+
+ )} + + {isCompleting && ( +
+ + Completing the login… +
+ )} + + {isStored && ( +
+ + Authenticated. The sandbox has credentials now. +
+ )} + + {isFailure && ( +
+ + {CLAUDE_LOGIN_FAILED_MESSAGE} +
+ )} + + {timedOut && !isFailure && !isStored && ( +
+ + {CLAUDE_LOGIN_TIMED_OUT_MESSAGE} +
+ )} +
+
+ ); +} + export function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestResult }) { const statusLabel = result.status === "pass" ? "Passed" : result.status === "warn" ? "Warnings" : "Failed"; diff --git a/ui/src/components/environment-variables-editor/model.ts b/ui/src/components/environment-variables-editor/model.ts index 182168a570..f0e7b09bcb 100644 --- a/ui/src/components/environment-variables-editor/model.ts +++ b/ui/src/components/environment-variables-editor/model.ts @@ -134,6 +134,33 @@ export function valueFromRows(rows: EnvRow[]): Record | unde return Object.keys(record).length > 0 ? record : undefined; } +// The fixed environment variable name for the Claude subscription OAuth token. +// A subscription login stores the token as a user secret under this key. The +// adapter reads the variable at run start. +export const CLAUDE_OAUTH_TOKEN_ENV_KEY = "CLAUDE_CODE_OAUTH_TOKEN"; + +/** + * Build the fixed `CLAUDE_CODE_OAUTH_TOKEN` binding. The binding is a + * `user_secret_ref`, so the adapter config holds a reference to the user secret, + * never the token value. This reuses the editor `user_secret_ref` builder in + * {@link valueFromRows}, so the fixed binding keeps the same shape as an + * operator-authored user-secret row. + */ +export function buildFixedClaudeOAuthBinding(): Record { + return ( + valueFromRows([ + { + ...emptyRow("user_secret"), + name: CLAUDE_OAUTH_TOKEN_ENV_KEY, + userSecretKey: CLAUDE_OAUTH_TOKEN_ENV_KEY, + required: true, + version: "latest", + }, + ]) ?? {} + ); +} + + export const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; export type NameIssueLevel = "error" | "warn"; diff --git a/ui/src/lib/new-agent-hire-payload.test.ts b/ui/src/lib/new-agent-hire-payload.test.ts index 0cbf68f8b4..6463022315 100644 --- a/ui/src/lib/new-agent-hire-payload.test.ts +++ b/ui/src/lib/new-agent-hire-payload.test.ts @@ -42,6 +42,32 @@ describe("buildNewAgentHirePayload", () => { }); }); + it("sends the apply-existing flag when the owner applies a stored Claude login", () => { + const payload = buildNewAgentHirePayload({ + name: "Stored Claude", + effectiveRole: "general", + configValues: { + ...defaultCreateValues, + adapterType: "claude_local", + claudeApplyStoredLogin: true, + }, + adapterConfig: {}, + }); + expect(payload).toMatchObject({ applyStoredClaudeLogin: true }); + // Apply-existing carries no stored-session claim. + expect("storedSessionId" in payload).toBe(false); + }); + + it("omits the apply-existing flag when the owner does not apply a stored login", () => { + const payload = buildNewAgentHirePayload({ + name: "Fresh Claude", + effectiveRole: "general", + configValues: { ...defaultCreateValues, adapterType: "claude_local" }, + adapterConfig: {}, + }); + expect("applyStoredClaudeLogin" in payload).toBe(false); + }); + it("includes core trust preset permissions when provided", () => { expect( buildNewAgentHirePayload({ diff --git a/ui/src/lib/new-agent-hire-payload.ts b/ui/src/lib/new-agent-hire-payload.ts index 6408cc8bee..e89e4d070e 100644 --- a/ui/src/lib/new-agent-hire-payload.ts +++ b/ui/src/lib/new-agent-hire-payload.ts @@ -40,5 +40,15 @@ export function buildNewAgentHirePayload(input: { }), budgetMonthlyCents: 0, ...(permissions ? { permissions } : {}), + // The stored-session claim is not an agent column. The server derives the + // owner from the actor and consumes the claim to bind the fixed OAuth token. + // Send it only after a Claude subscription login reaches the `stored` state. + ...(configValues.claudeStoredSessionId + ? { storedSessionId: configValues.claudeStoredSessionId } + : {}), + // The apply-existing flag is not an agent column. The server binds the fixed + // OAuth token reference to the owner stored value with no login round trip. + // Send it only after the owner applies an existing stored login. + ...(configValues.claudeApplyStoredLogin ? { applyStoredClaudeLogin: true } : {}), }; } diff --git a/ui/src/pages/Agents.test.tsx b/ui/src/pages/Agents.test.tsx index 7e01249a00..d883f5572e 100644 --- a/ui/src/pages/Agents.test.tsx +++ b/ui/src/pages/Agents.test.tsx @@ -193,6 +193,7 @@ const environmentCapabilities: EnvironmentCapabilities = { interactiveSetupConnectionTypes: [], supportsTemplateCapture: false, supportsTemplateDelete: false, + supportsSetupTokenLogin: false, displayName: "Fake", source: "builtin", }, @@ -206,6 +207,7 @@ const environmentCapabilities: EnvironmentCapabilities = { interactiveSetupConnectionTypes: ["ssh"], supportsTemplateCapture: true, supportsTemplateDelete: true, + supportsSetupTokenLogin: true, displayName: "Daytona", source: "plugin", }, diff --git a/ui/src/pages/NewAgent.test.tsx b/ui/src/pages/NewAgent.test.tsx new file mode 100644 index 0000000000..56160cd593 --- /dev/null +++ b/ui/src/pages/NewAgent.test.tsx @@ -0,0 +1,380 @@ +// @vitest-environment jsdom + +import { createRoot, type Root } from "react-dom/client"; +import { flushSync } from "react-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { getEnvironmentCapabilities } from "@paperclipai/shared"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { ToastProvider } from "../context/ToastContext"; +import { NewAgent } from "./NewAgent"; + +// The real adapter registry stays in place so the create request runs the real +// Claude config builder. That builder turns the fixed binding into the adapter +// `env` map, which is the contract this page test verifies. + +const mockAgentsApi = vi.hoisted(() => ({ + adapterModelProfiles: vi.fn(), + adapterModels: vi.fn(), + detectModel: vi.fn(), + list: vi.fn(), + hire: vi.fn(), + testEnvironment: vi.fn(), + getClaudeOAuthTokenStatus: vi.fn(), + startClaudeSetupTokenLogin: vi.fn(), + getClaudeSetupTokenLoginStatus: vi.fn(), + getClaudeSetupTokenLoginPrompt: vi.fn(), + submitClaudeSetupTokenBrowserCode: vi.fn(), + completeClaudeSetupTokenLogin: vi.fn(), + cancelClaudeSetupTokenLogin: vi.fn(), +})); + +const mockEnvironmentsApi = vi.hoisted(() => ({ list: vi.fn(), capabilities: vi.fn() })); +const mockInstanceSettingsApi = vi.hoisted(() => ({ + get: vi.fn(), + getExperimental: vi.fn(), + getGeneral: vi.fn(), +})); +const mockSecretsApi = vi.hoisted(() => ({ + list: vi.fn(), + listProposals: vi.fn(), + listUserSecretDefinitions: vi.fn(), +})); +const mockCompanySkillsApi = vi.hoisted(() => ({ list: vi.fn() })); +const mockIssuesApi = vi.hoisted(() => ({ list: vi.fn() })); +const mockProjectsApi = vi.hoisted(() => ({ list: vi.fn() })); +const mockAssetsApi = vi.hoisted(() => ({ uploadImage: vi.fn() })); +const mockClipboard = vi.hoisted(() => ({ copyTextToClipboard: vi.fn() })); +const navigateMock = vi.hoisted(() => vi.fn()); + +vi.mock("@/lib/router", () => ({ + useNavigate: () => navigateMock, + useSearchParams: () => [new URLSearchParams(), vi.fn()], +})); + +vi.mock("../context/CompanyContext", () => ({ + useCompany: () => ({ selectedCompanyId: "company-1" }), +})); + +vi.mock("../context/BreadcrumbContext", () => ({ + useBreadcrumbs: () => ({ setBreadcrumbs: vi.fn() }), +})); + +vi.mock("../api/agents", () => ({ agentsApi: mockAgentsApi })); +vi.mock("../api/environments", () => ({ environmentsApi: mockEnvironmentsApi })); +vi.mock("../api/instanceSettings", () => ({ instanceSettingsApi: mockInstanceSettingsApi })); +vi.mock("../api/secrets", () => ({ secretsApi: mockSecretsApi })); +vi.mock("../api/companySkills", () => ({ companySkillsApi: mockCompanySkillsApi })); +vi.mock("../api/issues", () => ({ issuesApi: mockIssuesApi })); +vi.mock("../api/projects", () => ({ projectsApi: mockProjectsApi })); +vi.mock("../api/assets", () => ({ assetsApi: mockAssetsApi })); +vi.mock("../lib/clipboard", () => ({ + copyTextToClipboard: mockClipboard.copyTextToClipboard, +})); + +vi.mock("../adapters/use-disabled-adapters", () => ({ + useDisabledAdaptersSync: () => new Set(), +})); + +vi.mock("../components/MarkdownEditor", () => ({ + MarkdownEditor: ({ + value, + onChange, + placeholder, + }: { + value: string; + onChange: (value: string) => void; + placeholder?: string; + }) => ( +