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; + }) => ( +