From 9a79fd857b79c696e8c2a7afcbc1930fffeaa6c8 Mon Sep 17 00:00:00 2001 From: Cheskie Londinsky Date: Thu, 20 Aug 2026 19:46:06 -0400 Subject: [PATCH 01/15] Borrow upstream security and correctness fixes Adapt OpenBot PRs #48, #51, #52, #54, and #58 for Kayco's managed agents, per-Bot computers, credential store, prompts, and Knowledge configuration. --- .env.example | 5 ++ README.md | 2 + agent-bot/src/index.ts | 11 +++ agent-langgraph/src/index.ts | 11 +++ deploy/hetzner/README.md | 1 + deploy/hetzner/compose.yaml | 2 + deploy/hetzner/env.example | 1 + docker-compose.yml | 2 + docs/configuration.md | 3 +- docs/coworkers.md | 5 +- examples/fintech/agents.yaml | 2 +- scripts/start.sh | 8 +- server/Dockerfile | 1 + server/src/agents/runtime-agents.ts | 13 ++++ server/src/computer/client.ts | 29 ++++--- server/src/computer/target.ts | 40 ++++++++++ server/src/config.ts | 3 + server/src/credentials.ts | 77 ++++++++++++++++--- server/src/index.ts | 5 +- server/tests/computer-client.test.ts | 21 +++++ server/tests/computer-target.test.ts | 46 ++++++++++- server/tests/config.test.ts | 13 ++++ server/tests/credentials.test.ts | 46 +++++++++++ .../tests/runtime-agents.integration.test.ts | 27 ++++++- server/tests/support/environment.ts | 1 + shared/agent-authorisation.test.ts | 37 +++++++++ shared/agent-authorisation.ts | 22 ++++++ shared/bot-prompt.test.ts | 25 ++++++ shared/bot-prompt.ts | 21 ++++- tests/compose.test.ts | 28 +++++++ 30 files changed, 480 insertions(+), 28 deletions(-) create mode 100644 shared/agent-authorisation.test.ts create mode 100644 shared/agent-authorisation.ts create mode 100644 shared/bot-prompt.test.ts diff --git a/.env.example b/.env.example index 7b60228..dcb2831 100644 --- a/.env.example +++ b/.env.example @@ -204,6 +204,11 @@ AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true # The managed coworker AG-UI endpoint. Required: use an HTTP(S) URL. MANAGED_AGENT_AG_UI_URL=http://localhost:4201/ag-ui +# Shared secret sent by the server on every call to a deployment-managed Bot, in the +# x-openbot-agent-token header. Both shipped Bots refuse to start without it. Generate one with: +# openssl rand -base64 32 +MANAGED_AGENT_TOKEN= + # The second Bot in the box runs on http://localhost:4201/ag-ui, on a framework rather than # proof of concept, and is reached the same way: point MANAGED_AGENT_AG_UI_URL at it, or add it as a # Bot of its own in the tenant package or at /agents. diff --git a/README.md b/README.md index c04b26c..3b39b7f 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui), 3. Fill the remaining required values: - `OPENAI_API_KEY` + - `MANAGED_AGENT_TOKEN` (generate with `openssl rand -base64 32`) Keep the managed Intelligence URLs from `.env.example` unless you run Intelligence yourself. The example `KEY_ENCRYPTION_KEY` is public and fine locally; generate your own with: @@ -186,6 +187,7 @@ See [docs/configuration.md](docs/configuration.md) and [docs/coworkers.md](docs/ - `DATABASE_URL` - `KEY_ENCRYPTION_KEY` - `MANAGED_AGENT_AG_UI_URL` +- `MANAGED_AGENT_TOKEN` - `INTELLIGENCE_API_URL` - `INTELLIGENCE_GATEWAY_WS_URL` - `INTELLIGENCE_API_KEY` diff --git a/agent-bot/src/index.ts b/agent-bot/src/index.ts index 56cf0df..d9c5ca1 100644 --- a/agent-bot/src/index.ts +++ b/agent-bot/src/index.ts @@ -2,6 +2,7 @@ import type { BaseEvent, RunAgentInput } from "@ag-ui/core"; import { EventEncoder } from "@ag-ui/encoder"; import { serve } from "bun"; import OpenAI from "openai"; +import { hasManagedAgentToken } from "../../shared/agent-authorisation"; import { SYSTEM_PROMPT } from "../../shared/bot-prompt"; /** @@ -16,6 +17,13 @@ import { SYSTEM_PROMPT } from "../../shared/bot-prompt"; */ const PORT = Number.parseInt(process.env.PORT ?? "4200", 10); +const MANAGED_AGENT_TOKEN = process.env.MANAGED_AGENT_TOKEN?.trim(); +if (!MANAGED_AGENT_TOKEN) { + console.error( + "MANAGED_AGENT_TOKEN is not set. This Bot holds a model credential and will not start without a token for OpenBot's server.", + ); + process.exit(1); +} /** * Which model drives the Bot. * @@ -224,6 +232,9 @@ serve({ } if (url.pathname === "/ag-ui" && request.method === "POST") { + if (!hasManagedAgentToken(request, MANAGED_AGENT_TOKEN)) { + return Response.json({ error: "Unauthorized." }, { status: 401 }); + } const input = (await request.json()) as RunAgentInput; return runAgent(input); } diff --git a/agent-langgraph/src/index.ts b/agent-langgraph/src/index.ts index 87f95d3..a6c7d4c 100644 --- a/agent-langgraph/src/index.ts +++ b/agent-langgraph/src/index.ts @@ -18,6 +18,7 @@ import { import { ChatOpenAI } from "@langchain/openai"; import { ChatXAI } from "@langchain/xai"; import { serve } from "bun"; +import { hasManagedAgentToken } from "../../shared/agent-authorisation"; import { SYSTEM_PROMPT } from "../../shared/bot-prompt"; /** @@ -40,6 +41,13 @@ import { SYSTEM_PROMPT } from "../../shared/bot-prompt"; */ const PORT = Number.parseInt(process.env.PORT ?? "4201", 10); +const MANAGED_AGENT_TOKEN = process.env.MANAGED_AGENT_TOKEN?.trim(); +if (!MANAGED_AGENT_TOKEN) { + console.error( + "MANAGED_AGENT_TOKEN is not set. This Bot holds a model credential and will not start without a token for OpenBot's server.", + ); + process.exit(1); +} /** * Which model drives this Bot, and from whom. @@ -570,6 +578,9 @@ serve({ } if (url.pathname === "/ag-ui" && request.method === "POST") { + if (!hasManagedAgentToken(request, MANAGED_AGENT_TOKEN)) { + return Response.json({ error: "Unauthorized." }, { status: 401 }); + } const input = (await request.json()) as RunAgentInput; return runAgent(input); } diff --git a/deploy/hetzner/README.md b/deploy/hetzner/README.md index 488dc3c..ba569b8 100644 --- a/deploy/hetzner/README.md +++ b/deploy/hetzner/README.md @@ -63,6 +63,7 @@ openssl rand -base64 48 # BETTER_AUTH_SECRET openssl rand -hex 32 # POSTGRES_PASSWORD openssl rand -hex 32 # COMPUTER_TOKEN openssl rand -hex 32 # SUPERVISOR_TOKEN +openssl rand -base64 32 # MANAGED_AGENT_TOKEN ``` Fill the remaining settings in `env.production`. In Google Cloud, register this redirect URI: diff --git a/deploy/hetzner/compose.yaml b/deploy/hetzner/compose.yaml index 27fc6af..d68521f 100644 --- a/deploy/hetzner/compose.yaml +++ b/deploy/hetzner/compose.yaml @@ -106,6 +106,7 @@ services: CODEX_PROCESS_IDLE_MS: ${CODEX_PROCESS_IDLE_MS:-300000} CODEX_DEFAULT_MODEL: ${CODEX_DEFAULT_MODEL:-} MANAGED_AGENT_AG_UI_URL: http://agent-langgraph:4201/ag-ui + MANAGED_AGENT_TOKEN: ${MANAGED_AGENT_TOKEN:?Set MANAGED_AGENT_TOKEN in env.production} OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GOOGLE_API_KEY: ${GOOGLE_API_KEY:-} @@ -143,6 +144,7 @@ services: restart: unless-stopped environment: PORT: "4201" + MANAGED_AGENT_TOKEN: ${MANAGED_AGENT_TOKEN:?Set MANAGED_AGENT_TOKEN in env.production} BOT_PROVIDER: ${BOT_PROVIDER:-openai} BOT_MODEL: ${BOT_MODEL:-} BOT_RESPONSES_API: ${BOT_RESPONSES_API:-false} diff --git a/deploy/hetzner/env.example b/deploy/hetzner/env.example index 1241104..2732900 100644 --- a/deploy/hetzner/env.example +++ b/deploy/hetzner/env.example @@ -17,6 +17,7 @@ BETTER_AUTH_SECRET=replace-me BETTER_AUTH_CROSS_SITE_COOKIES=false COMPUTER_TOKEN=replace-me SUPERVISOR_TOKEN=replace-me +MANAGED_AGENT_TOKEN=replace-me GOOGLE_OAUTH_CLIENT_ID=replace-me GOOGLE_OAUTH_CLIENT_SECRET=replace-me diff --git a/docker-compose.yml b/docker-compose.yml index 1803b83..e42b9d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -185,6 +185,7 @@ services: - "${BOT_PORT:-4200}:4200" environment: OPENAI_API_KEY: ${OPENAI_API_KEY} + MANAGED_AGENT_TOKEN: ${MANAGED_AGENT_TOKEN:-} # Unset means OpenAI. Set, it is any endpoint speaking the same API, and BOT_MODEL is sent # to it verbatim. OPENAI_BASE_URL: ${OPENAI_BASE_URL:-} @@ -206,6 +207,7 @@ services: # The selected provider reads its own key. Models requiring the Responses API use # BOT_RESPONSES_API instead of changing the streaming loop here. BOT_PROVIDER: ${BOT_PROVIDER:-openai} + MANAGED_AGENT_TOKEN: ${MANAGED_AGENT_TOKEN:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_BASE_URL: ${OPENAI_BASE_URL:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} diff --git a/docs/configuration.md b/docs/configuration.md index 696fc43..da806e1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,6 +21,7 @@ bash scripts/start.sh | `DATABASE_URL` | PostgreSQL connection string. | | `KEY_ENCRYPTION_KEY` | Base64-encoded 32-byte key for encrypted stored credentials. Generate with `openssl rand -base64 32`. | | `MANAGED_AGENT_AG_UI_URL` | Default AG-UI endpoint for coworkers created in the product. Must be HTTP(S). | +| `MANAGED_AGENT_TOKEN` | Secret sent only to the managed AG-UI endpoint. Generate with `openssl rand -base64 32`. | | `INTELLIGENCE_API_URL` | CopilotKit Intelligence API URL. | | `INTELLIGENCE_GATEWAY_WS_URL` | CopilotKit Intelligence realtime gateway URL. | | `INTELLIGENCE_API_KEY` | Runtime key for the Intelligence project. | @@ -276,7 +277,7 @@ agents: role_description: Answer company knowledge questions and cite sources. avatar_seed: knowledge type: built-in - system_prompt: Answer from authorized company knowledge and cite every source. + system_prompt: Answer only from authorized company knowledge and cite every source you use. If no authorized source is connected or the answer is not present, say so plainly instead of guessing. - id: risk-analyst name: Risk Analyst diff --git a/docs/coworkers.md b/docs/coworkers.md index 3e9fb48..04acbd3 100644 --- a/docs/coworkers.md +++ b/docs/coworkers.md @@ -68,9 +68,12 @@ Product-created coworkers use: ```dotenv MANAGED_AGENT_AG_UI_URL=http://localhost:4201/ag-ui +MANAGED_AGENT_TOKEN= ``` -The server requires this setting at startup. Package-provided agents use their own `agents.yaml` configuration. +The server and both shipped Bots require the token at startup. The server sends it only to the +managed endpoint; customer-owned endpoints never receive it. Package-provided agents use their own +`agents.yaml` configuration. ## Register an external AG-UI agent diff --git a/examples/fintech/agents.yaml b/examples/fintech/agents.yaml index f5818f7..29c30c8 100644 --- a/examples/fintech/agents.yaml +++ b/examples/fintech/agents.yaml @@ -14,7 +14,7 @@ agents: role_description: Help answer company knowledge questions and cite sources when available. avatar_seed: knowledge type: built-in - system_prompt: Answer from authorized company knowledge and cite every source. + system_prompt: Answer only from authorized company knowledge and cite every source you use. If no authorized source is connected or the answer is not present, say so plainly instead of guessing. - id: research-analyst name: Research Analyst title: Evidence & Analysis diff --git a/scripts/start.sh b/scripts/start.sh index 745faf4..386af15 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -35,11 +35,17 @@ ONE_COMPUTER_EACH="${OPENBOT_ONE_COMPUTER_EACH:-true}" export APP_PORT SERVER_PORT SUPERVISOR_TOKEN="$(setting SUPERVISOR_TOKEN openbot-dev-supervisor-token)" COMPUTER_TOKEN="$(setting COMPUTER_TOKEN openbot-dev-computer-token)" +MANAGED_AGENT_TOKEN="$(setting MANAGED_AGENT_TOKEN '')" green() { printf '\033[32m%s\033[0m\n' "$1"; } red() { printf '\033[31m%s\033[0m\n' "$1"; } info() { printf '\033[2m%s\033[0m\n' "$1"; } +if [ -z "$MANAGED_AGENT_TOKEN" ]; then + red " MANAGED_AGENT_TOKEN is not set in .env. Generate one with: openssl rand -base64 32" + exit 1 +fi + holder() { lsof -nP -iTCP:"$1" -sTCP:LISTEN -Fcn 2>/dev/null | awk '/^c/{c=substr($0,2)} /^n/{print c" ("substr($0,2)")"; exit}' || true } @@ -88,7 +94,7 @@ for svc_port in "agent-computer:$COMPUTER_PORT" "agent-bot:$BOT_PORT" "agent-lan fi done -export SUPERVISOR_TOKEN COMPUTER_TOKEN +export SUPERVISOR_TOKEN COMPUTER_TOKEN MANAGED_AGENT_TOKEN export COMPUTER_PORT BOT_PORT LANGGRAPH_PORT SUPERVISOR_PORT docker compose up -d --build "${SERVICES[@]}" >/dev/null if ! docker compose run --rm --build migrate >"$LOGS/migrate.log" 2>&1; then diff --git a/server/Dockerfile b/server/Dockerfile index a3c4de7..8ddb456 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -18,6 +18,7 @@ RUN bun install --frozen-lockfile COPY server server COPY sdk sdk +COPY shared shared COPY examples examples WORKDIR /app/server diff --git a/server/src/agents/runtime-agents.ts b/server/src/agents/runtime-agents.ts index 257900c..f5a5f78 100644 --- a/server/src/agents/runtime-agents.ts +++ b/server/src/agents/runtime-agents.ts @@ -2,6 +2,7 @@ import { and, eq, isNotNull, isNull, or } from "drizzle-orm"; import { type RegisteredAgent, registeredAgentFromRow } from "../copilot"; import type { CredentialSecretReader } from "../credentials"; import type { Database } from "../db/client"; +import { MANAGED_AGENT_TOKEN_HEADER } from "../../../shared/agent-authorisation"; import { agentProfiles, agents, @@ -22,6 +23,8 @@ export function createRuntimeAgentLoader( database: Database, /** Resolves a customer agent's key at load time. Absent means no agent can carry one. */ vault?: { reader: CredentialSecretReader; encryptionKey: string }, + /** Deployment secret for the managed Bot. Never sent to a customer-owned endpoint. */ + managedAgent?: { endpoint: URL; token: string }, ) { return async (actor: AgentActor): Promise => { const [active, tombstones] = await Promise.all([ @@ -45,6 +48,16 @@ export function createRuntimeAgentLoader( }); if (headers) agent.headers = headers; } + if ( + agent.type === "remote_ag_ui" && + managedAgent && + agent.endpoint === managedAgent.endpoint.toString() + ) { + agent.headers = { + ...agent.headers, + [MANAGED_AGENT_TOKEN_HEADER]: managedAgent.token, + }; + } registered.set(agent.id, agent); } for (const row of tombstones) { diff --git a/server/src/computer/client.ts b/server/src/computer/client.ts index 9e14f59..448a713 100644 --- a/server/src/computer/client.ts +++ b/server/src/computer/client.ts @@ -33,7 +33,7 @@ import type { WriteFileInput, WriteFileResult, } from "./schema"; -import { checkNavigationTarget } from "./target"; +import { checkComputerAddress, checkNavigationTarget } from "./target"; /** * How the server talks to a Bot's computer. @@ -240,14 +240,25 @@ export function createComputerClient(options: ComputerClientOptions) { let target: string; const locateStartedAt = Date.now(); - try { - target = - botId && options.resolveBaseUrl - ? (await options.resolveBaseUrl(botId)).replace(/\/$/, "") - : base; - } catch (error) { - if (attempt + 1 < attempts) continue; - throw error; + if (botId && options.resolveBaseUrl) { + let located: string; + try { + located = (await options.resolveBaseUrl(botId)).replace(/\/$/, ""); + } catch (error) { + if (attempt + 1 < attempts) continue; + throw error; + } + + // A hosted supervisor controls this value, and the next request carries the deployment's + // computer token. Private addresses are valid for our local supervisor; metadata addresses + // and non-web schemes are not valid computer endpoints under any configuration. + const verdict = checkComputerAddress(located); + if (!verdict.allowed) { + throw new ComputerUnavailableError(verdict.reason); + } + target = located; + } else { + target = base; } const locateMs = Date.now() - locateStartedAt; diff --git a/server/src/computer/target.ts b/server/src/computer/target.ts index 1a3297e..0ed85b1 100644 --- a/server/src/computer/target.ts +++ b/server/src/computer/target.ts @@ -178,6 +178,46 @@ function isPrivateIpv4(hostname: string): boolean { return false; } +/** + * Decide whether an address returned by a computer supervisor may be called. + * + * This is deliberately narrower than {@link checkNavigationTarget}. A supervisor is expected to + * return private or loopback addresses for locally managed computers, so those must remain valid. + * The invariants that always hold are that the address speaks HTTP(S) and never names a cloud + * metadata endpoint: the next request carries this deployment's computer token. + */ +export function checkComputerAddress(raw: string): TargetVerdict { + let url: URL; + try { + url = new URL(raw); + } catch { + return { + allowed: false, + reason: `The computer's address is not a URL: ${raw}`, + }; + } + + if (!ALLOWED_PROTOCOLS.has(url.protocol)) { + return { + allowed: false, + reason: `A computer must be reached over http or https, not ${url.protocol.replace(":", "")}.`, + }; + } + + // Use the same canonicalization as navigation checks so alternate IPv6 spellings, embedded IPv4 + // addresses and DNS root dots cannot turn a denied metadata address into a different string. + const hostname = canonicalHostname(url.hostname.toLowerCase()); + if (NEVER_ALLOWED_HOSTNAMES.has(hostname)) { + return { + allowed: false, + reason: + "That address holds this deployment's own cloud credentials, so it is never called as a computer.", + }; + } + + return { allowed: true, url: url.toString() }; +} + /** * Decide whether a Bot may navigate here. * diff --git a/server/src/config.ts b/server/src/config.ts index c308862..7dea637 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -29,6 +29,8 @@ export type DeploymentConfig = { databaseUrl: string; keyEncryptionKey: string; managedAgentAgUiUrl: URL; + /** Secret sent only to the deployment-managed Bot endpoint. */ + managedAgentToken: string; /** Browser application origins allowed to make credentialed HTTP and WebSocket requests. */ trustedOrigins: string[]; /** @@ -517,6 +519,7 @@ export function loadConfig( environment, "MANAGED_AGENT_AG_UI_URL", ), + managedAgentToken: required(environment, "MANAGED_AGENT_TOKEN"), deploymentId: optional(environment, "DEPLOYMENT_ID"), tenantPackageDirectory: optional(environment, "TENANT_PACKAGE_DIR") ?? "../examples/fintech", diff --git a/server/src/credentials.ts b/server/src/credentials.ts index 1e2afdd..1709f93 100644 --- a/server/src/credentials.ts +++ b/server/src/credentials.ts @@ -32,16 +32,23 @@ type StoredCredential = { revokedAt: Date | null; }; +type CredentialWrite = { + kind: CredentialKind; + provider: string; + keyId: string; + metadata: Record; + encryptedValue: string; +}; + export type CredentialStore = { - create: (value: { - kind: CredentialKind; - provider: string; - keyId: string; - metadata: Record; - encryptedValue: string; - }) => Promise; + create: (value: CredentialWrite) => Promise; revoke: (id: string) => Promise; replaceEncryptedValue: (id: string, encryptedValue: string) => Promise; + /** Atomically create the replacement and revoke the previous credential when the store can. */ + rotate?: ( + previousCredentialId: string, + value: CredentialWrite, + ) => Promise; }; export type CredentialSecretReader = { @@ -174,6 +181,28 @@ export function createCredentialStore( } return credential; }, + rotate: async (previousCredentialId, value) => + database.transaction(async (transaction) => { + const [credential] = await transaction + .insert(credentials) + .values(value) + .returning({ id: credentials.id, revokedAt: credentials.revokedAt }); + if (!credential) { + throw new Error("Credential could not be stored"); + } + + const revokedAt = new Date(); + const [previous] = await transaction + .update(credentials) + .set({ revokedAt, updatedAt: revokedAt }) + .where(eq(credentials.id, previousCredentialId)) + .returning({ id: credentials.id }); + if (!previous) { + throw new Error("Credential was not found"); + } + + return credential; + }), revoke: async (id) => { const revokedAt = new Date(); const [credential] = await database @@ -319,8 +348,38 @@ export async function rotateCredential( service: CredentialService, input: CredentialInput & { previousCredentialId: string }, ) { - const credential = await persistCredential(service, input); - await service.store.revoke(input.previousCredentialId); + let credential: CredentialStatus; + if (service.store.rotate) { + const stored = await service.store.rotate(input.previousCredentialId, { + kind: input.kind, + provider: input.provider, + keyId: input.keyId, + metadata: input.metadata, + encryptedValue: await encryptSecret( + service.encryptionKey, + input.plaintext, + ), + }); + credential = { + id: stored.id, + kind: input.kind, + provider: input.provider, + keyId: input.keyId, + metadata: input.metadata, + revokedAt: stored.revokedAt, + }; + } else { + credential = await persistCredential(service, input); + try { + await service.store.revoke(input.previousCredentialId); + } catch (error) { + // Generic stores cannot promise a transaction. Compensate by revoking the replacement so a + // failed rotation does not leave an unlinked active secret, and do not audit a rotation that + // did not finish. + await service.store.revoke(credential.id).catch(() => {}); + throw error; + } + } await recordAuditEvent(service.auditStore, { eventType: "credential.rotated", diff --git a/server/src/index.ts b/server/src/index.ts index 5c66ede..bcf3265 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -232,7 +232,10 @@ const channelActivityListener = await startChannelActivityListener( channelEvents, ); const roleRepository = createRoleRepository(database); -const loadAgentsForActor = createRuntimeAgentLoader(database, agentVault); +const loadAgentsForActor = createRuntimeAgentLoader(database, agentVault, { + endpoint: config.managedAgentAgUiUrl, + token: config.managedAgentToken, +}); await synchronizeTenantPackage(database, tenantPackage); const auth = config.auth ? createAuth(config, database) : undefined; // One computer each, when a supervisor is configured to give them out. Without one every Bot shares diff --git a/server/tests/computer-client.test.ts b/server/tests/computer-client.test.ts index 8d2a34d..d7a7c86 100644 --- a/server/tests/computer-client.test.ts +++ b/server/tests/computer-client.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { + ComputerUnavailableError, createComputerClient, ElementNotFoundError, HumanControlError, @@ -27,6 +28,26 @@ const ok = (body: unknown) => }); describe("computer client", () => { + test("never sends the computer token to an address refused by the supervisor boundary", async () => { + let called = false; + const client = createComputerClient({ + baseUrl: "http://agent-computer:4100", + token: "deployment-computer-token", + allowPrivateHosts: false, + resolveBaseUrl: async () => + "http://[::ffff:169.254.169.254]/latest/meta-data", + fetchImpl: (() => { + called = true; + return Promise.resolve(ok({})); + }) as unknown as typeof fetch, + }).forBot("bot-1"); + + await expect(client.read()).rejects.toBeInstanceOf( + ComputerUnavailableError, + ); + expect(called).toBe(false); + }); + test("navigates and returns where it landed", async () => { const seen: string[] = []; const client = clientWith((url, init) => { diff --git a/server/tests/computer-target.test.ts b/server/tests/computer-target.test.ts index 5a56598..f5fe9c3 100644 --- a/server/tests/computer-target.test.ts +++ b/server/tests/computer-target.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { checkNavigationTarget } from "../src/computer/target"; +import { + checkComputerAddress, + checkNavigationTarget, +} from "../src/computer/target"; describe("navigation targets", () => { test("allows an ordinary public address", () => { @@ -139,3 +142,44 @@ describe("navigation targets", () => { expect(checkNavigationTarget("http://172.31.255.255/").allowed).toBe(false); }); }); + +describe("computer addresses returned by a supervisor", () => { + test("allows the private address the local supervisor returns", () => { + expect(checkComputerAddress("http://127.0.0.1:49213")).toEqual({ + allowed: true, + url: "http://127.0.0.1:49213/", + }); + }); + + test("allows a hosted provider's public address", () => { + expect( + checkComputerAddress("https://sandbox-abc123.daytona.app").allowed, + ).toBe(true); + }); + + test.each([ + "169.254.169.254", + "metadata.google.internal", + "metadata.google.internal.", + "metadata.goog", + "[fd00:ec2::254]", + "[::ffff:169.254.169.254]", + ])("refuses the cloud metadata address %s", (host) => { + const verdict = checkComputerAddress(`http://${host}/latest/meta-data/`); + expect(verdict.allowed).toBe(false); + if (!verdict.allowed) expect(verdict.reason).toContain("cloud credentials"); + }); + + test.each(["file:///etc/passwd", "ftp://example.com", "gopher://x"])( + "refuses the non-web address %s", + (raw) => { + expect(checkComputerAddress(raw).allowed).toBe(false); + }, + ); + + test("refuses something that is not a URL", () => { + const verdict = checkComputerAddress("not-an-address"); + expect(verdict.allowed).toBe(false); + if (!verdict.allowed) expect(verdict.reason).toContain("not a URL"); + }); +}); diff --git a/server/tests/config.test.ts b/server/tests/config.test.ts index 44b6fa0..02d8b5a 100644 --- a/server/tests/config.test.ts +++ b/server/tests/config.test.ts @@ -17,6 +17,7 @@ const baseEnvironment = { INTELLIGENCE_API_KEY: "tenant-api-key", COPILOTKIT_LICENSE_TOKEN: "license-token", MANAGED_AGENT_AG_UI_URL: " http://localhost:4200/ag-ui ", + MANAGED_AGENT_TOKEN: "managed-agent-token", }; describe("deployment configuration", () => { @@ -36,6 +37,7 @@ describe("deployment configuration", () => { expect(config.managedAgentAgUiUrl).toEqual( new URL("http://localhost:4200/ag-ui"), ); + expect(config.managedAgentToken).toBe("managed-agent-token"); expect(config.tenantPackageDirectory).toBe("../examples/fintech"); expect(config.agentToolToken).toBeUndefined(); }); @@ -58,6 +60,7 @@ describe("deployment configuration", () => { INTELLIGENCE_API_KEY: baseEnvironment.INTELLIGENCE_API_KEY, COPILOTKIT_LICENSE_TOKEN: baseEnvironment.COPILOTKIT_LICENSE_TOKEN, MANAGED_AGENT_AG_UI_URL: baseEnvironment.MANAGED_AGENT_AG_UI_URL, + MANAGED_AGENT_TOKEN: baseEnvironment.MANAGED_AGENT_TOKEN, }); expect(config.auth).toBeUndefined(); @@ -88,6 +91,7 @@ describe("deployment configuration", () => { DATABASE_URL: baseEnvironment.DATABASE_URL, KEY_ENCRYPTION_KEY: baseEnvironment.KEY_ENCRYPTION_KEY, MANAGED_AGENT_AG_UI_URL: baseEnvironment.MANAGED_AGENT_AG_UI_URL, + MANAGED_AGENT_TOKEN: baseEnvironment.MANAGED_AGENT_TOKEN, }), ).toThrow("CopilotKit Intelligence is required and is not configured"); }); @@ -122,6 +126,15 @@ describe("deployment configuration", () => { ).toThrow("MANAGED_AGENT_AG_UI_URL"); }); + test("refuses to start when MANAGED_AGENT_TOKEN is missing", () => { + const environment: Record = { + ...baseEnvironment, + }; + delete environment.MANAGED_AGENT_TOKEN; + + expect(() => loadConfig(environment)).toThrow("MANAGED_AGENT_TOKEN"); + }); + test("requires a base64-encoded 32-byte key-encryption key", () => { expect(() => loadConfig({ diff --git a/server/tests/credentials.test.ts b/server/tests/credentials.test.ts index cb3e509..5fe6d6a 100644 --- a/server/tests/credentials.test.ts +++ b/server/tests/credentials.test.ts @@ -138,6 +138,52 @@ describe("credential encryption", () => { ).toEqual(["credential.rotated", "credential.revoked"]); }); + test("rolls back the replacement when a non-transactional store cannot revoke the old credential", async () => { + const created: string[] = []; + const revoked: string[] = []; + const audited: unknown[] = []; + const service = { + encryptionKey: key, + store: { + create: async () => { + const id = "credential-new"; + created.push(id); + return { id, revokedAt: null }; + }, + revoke: async (id: string) => { + revoked.push(id); + if (id === "credential-old") { + throw new Error("Previous credential not found"); + } + return new Date("2026-08-13T12:00:00.000Z"); + }, + }, + auditStore: { + insert: async (event: unknown) => { + audited.push(event); + }, + }, + }; + + await expect( + rotateCredential(service, { + previousCredentialId: "credential-old", + kind: "model", + provider: "openai", + keyId: "primary", + metadata: {}, + plaintext: "new-openai-secret", + actorUserId: "admin", + }), + ).rejects.toThrow("Previous credential not found"); + + expect(created).toEqual(["credential-new"]); + expect(revoked).toEqual(["credential-old", "credential-new"]); + expect( + audited.map((event) => (event as { eventType: string }).eventType), + ).not.toContain("credential.rotated"); + }); + test("decrypts only an active credential for server-side use", async () => { const encryptedValue = await encryptSecret(key, "connector-secret"); diff --git a/server/tests/runtime-agents.integration.test.ts b/server/tests/runtime-agents.integration.test.ts index 0756a33..3eabd13 100644 --- a/server/tests/runtime-agents.integration.test.ts +++ b/server/tests/runtime-agents.integration.test.ts @@ -22,13 +22,17 @@ const databaseUrl = "postgres://openbot:openbot@localhost:5432/openbot"; const database = createDatabase(databaseUrl, TEST_POOL); const managedEndpoint = new URL("https://managed.example.test/ag-ui"); +const managedAgentToken = "managed-agent-token"; const profileStore = createAgentProfileStore(database, managedEndpoint); const channelStore = createChannelStore( database, profileStore, createThreadIdentity("test-deployment"), ); -const loadAgents = createRuntimeAgentLoader(database); +const loadAgents = createRuntimeAgentLoader(database, undefined, { + endpoint: managedEndpoint, + token: managedAgentToken, +}); const testPrefix = `runtime-agents-${randomUUID()}`; const createdUserIds: string[] = []; @@ -70,7 +74,11 @@ async function createUser(role: AgentActor["role"] = "user") { async function createCoworker( owner: AgentActor, - overrides: { name?: string; visibility?: "public" | "private" } = {}, + overrides: { + name?: string; + visibility?: "public" | "private"; + endpoint?: string; + } = {}, ) { const profile = await profileStore.create(owner, { name: overrides.name ?? "Expense Manager", @@ -78,6 +86,7 @@ async function createCoworker( roleDescription: "Review receipts, categorize expenses, and prepare reimbursement reports.", visibility: overrides.visibility ?? "private", + ...(overrides.endpoint ? { endpoint: overrides.endpoint } : {}), }); createdAgentIds.push(profile.id); return profile; @@ -104,6 +113,7 @@ describe("runtime agent loading", () => { name: "Expense Manager", type: "remote_ag_ui", endpoint: managedEndpoint.toString(), + headers: { "x-openbot-agent-token": managedAgentToken }, standingMessage: standingRoleMessage({ id: profile.id, name: "Expense Manager", @@ -114,6 +124,19 @@ describe("runtime agent loading", () => { }); }); + test("never sends the managed token to a customer-owned endpoint", async () => { + const owner = await createUser(); + const endpoint = "https://customer.example.test/ag-ui"; + const profile = await createCoworker(owner, { endpoint }); + + const loaded = (await loadAgents(owner)).find( + (agent) => agent.id === profile.id, + ); + + expect(loaded).toMatchObject({ endpoint }); + expect(loaded?.type === "remote_ag_ui" && loaded.headers).toBeUndefined(); + }); + test("hides a private coworker from everybody but its owner and administrators", async () => { const owner = await createUser(); const otherUser = await createUser(); diff --git a/server/tests/support/environment.ts b/server/tests/support/environment.ts index 2422ef7..6912067 100644 --- a/server/tests/support/environment.ts +++ b/server/tests/support/environment.ts @@ -23,6 +23,7 @@ export function testEnvironment( INTELLIGENCE_API_KEY: "tenant-api-key", COPILOTKIT_LICENSE_TOKEN: "license-token", MANAGED_AGENT_AG_UI_URL: "http://localhost:4200/ag-ui", + MANAGED_AGENT_TOKEN: "managed-agent-token", ...overrides, }; } diff --git a/shared/agent-authorisation.test.ts b/shared/agent-authorisation.test.ts new file mode 100644 index 0000000..98e7ee9 --- /dev/null +++ b/shared/agent-authorisation.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "bun:test"; +import { + hasManagedAgentToken, + MANAGED_AGENT_TOKEN_HEADER, + matchesToken, +} from "./agent-authorisation"; + +describe("managed agent authorization", () => { + test("accepts only the configured token", () => { + const expected = "agent-bot-secret"; + + expect( + hasManagedAgentToken( + new Request("http://bot.local/ag-ui", { + headers: { [MANAGED_AGENT_TOKEN_HEADER]: expected }, + }), + expected, + ), + ).toBe(true); + expect( + hasManagedAgentToken(new Request("http://bot.local/ag-ui"), expected), + ).toBe(false); + expect( + hasManagedAgentToken( + new Request("http://bot.local/ag-ui", { + headers: { [MANAGED_AGENT_TOKEN_HEADER]: "wrong" }, + }), + expected, + ), + ).toBe(false); + }); + + test("rejects empty and differently sized tokens", () => { + expect(matchesToken("", "")).toBe(false); + expect(matchesToken("expected", "short")).toBe(false); + }); +}); diff --git a/shared/agent-authorisation.ts b/shared/agent-authorisation.ts new file mode 100644 index 0000000..7060302 --- /dev/null +++ b/shared/agent-authorisation.ts @@ -0,0 +1,22 @@ +export const MANAGED_AGENT_TOKEN_HEADER = "x-openbot-agent-token"; + +/** Compare a caller's managed-Bot token without leaking its contents through timing. */ +export function matchesToken(expected: string, offered: string): boolean { + if (expected.length === 0 || offered.length !== expected.length) return false; + let difference = 0; + for (let index = 0; index < offered.length; index += 1) { + difference |= offered.charCodeAt(index) ^ expected.charCodeAt(index); + } + return difference === 0; +} + +/** Accept the one server-to-Bot credential used by deployment-managed Bots. */ +export function hasManagedAgentToken( + request: Request, + expected: string, +): boolean { + return matchesToken( + expected, + request.headers.get(MANAGED_AGENT_TOKEN_HEADER)?.trim() ?? "", + ); +} diff --git a/shared/bot-prompt.test.ts b/shared/bot-prompt.test.ts new file mode 100644 index 0000000..b9c996f --- /dev/null +++ b/shared/bot-prompt.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test"; +import { SYSTEM_PROMPT } from "./bot-prompt"; + +describe("SYSTEM_PROMPT", () => { + test("keeps paragraph breaks instead of collapsing them into spaces", () => { + expect(SYSTEM_PROMPT).toContain("\n\n"); + expect(SYSTEM_PROMPT).not.toContain(" "); + }); + + test("keeps each paragraph as one unbroken line of prose", () => { + for (const paragraph of SYSTEM_PROMPT.split("\n\n")) { + expect(paragraph).not.toContain("\n"); + expect(paragraph.length).toBeGreaterThan(0); + } + }); + + test("keeps the instruction wording", () => { + expect(SYSTEM_PROMPT).toContain( + "You are a Bot with your own computer, a real web browser the person can watch you use.", + ); + expect(SYSTEM_PROMPT).toContain( + "Say what you found or did in plain language, briefly.", + ); + }); +}); diff --git a/shared/bot-prompt.ts b/shared/bot-prompt.ts index 8e92490..f6fca57 100644 --- a/shared/bot-prompt.ts +++ b/shared/bot-prompt.ts @@ -10,7 +10,7 @@ * The prompt requires snapshot-first computer use. Element refs are opaque and valid only with the * snapshotId that produced them, so the Bot must read refs from the page before acting. */ -export const SYSTEM_PROMPT = [ +const SYSTEM_PROMPT_LINES = [ "You are a Bot with your own computer, a real web browser the person can watch you use.", "When you are asked to look at, open, visit, check or read a web page, call computer_navigate.", "Never claim you cannot browse: opening a page is something you can actually do.", @@ -53,4 +53,21 @@ export const SYSTEM_PROMPT = [ "something to retry: say plainly what was blocked and why, and stop. Do not try another route to", "the same thing.", "Say what you found or did in plain language, briefly.", -].join(" "); +]; + +/** + * Empty entries mark paragraph breaks. Build each paragraph as one line of prose, then retain the + * blank line between sections instead of collapsing the entire prompt into a run-on block. + */ +export const SYSTEM_PROMPT = SYSTEM_PROMPT_LINES.reduce( + (paragraphs, line) => { + if (line === "") { + paragraphs.push(""); + return paragraphs; + } + const last = paragraphs.length - 1; + paragraphs[last] = paragraphs[last] ? `${paragraphs[last]} ${line}` : line; + return paragraphs; + }, + [""], +).join("\n\n"); diff --git a/tests/compose.test.ts b/tests/compose.test.ts index 282af72..7062b27 100644 --- a/tests/compose.test.ts +++ b/tests/compose.test.ts @@ -62,6 +62,34 @@ test("gives both shipped Bots the OpenAI-compatible endpoint", () => { } }); +test("gives the server and managed Bots the same request credential", () => { + const local = readFileSync( + join(import.meta.dir, "..", "docker-compose.yml"), + "utf8", + ); + const production = readFileSync( + join(import.meta.dir, "..", "deploy", "hetzner", "compose.yaml"), + "utf8", + ); + + // Local server runs on the host and reads .env directly; both containerized Bots need Compose to + // pass the value through. Production containerizes both sides and requires the secret explicitly. + expect( + local.match(/MANAGED_AGENT_TOKEN: \$\{MANAGED_AGENT_TOKEN:-\}/g), + ).toHaveLength(2); + expect( + production.match( + /MANAGED_AGENT_TOKEN: \$\{MANAGED_AGENT_TOKEN:\?Set MANAGED_AGENT_TOKEN in env\.production\}/g, + ), + ).toHaveLength(2); + + const serverDockerfile = readFileSync( + join(import.meta.dir, "..", "server", "Dockerfile"), + "utf8", + ); + expect(serverDockerfile).toContain("COPY shared shared"); +}); + test("enables pgvector before creating vector columns", () => { const migration = readFileSync( join(import.meta.dir, "..", "server", "drizzle", "0000_schema.sql"), From cf59362e48fd9928f64349366cb8d0623d35e1ae Mon Sep 17 00:00:00 2001 From: Hotragn Pettugani <103170876+Hotragn@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:59:41 -0400 Subject: [PATCH 02/15] Keep the playground's delete off a component the build ships (#109) `DELETE /api/sandboxed/:name` handed the raw name to `store.remove`, which deleted from the shared `components` table with nothing checking which kind of component the name belonged to. `save` refuses a name that is not a slug and `publish` refuses a name with no draft behind it; `remove` did neither, and that asymmetry was the bug rather than the delete itself. Naming a compiled component removed its governance row, and the foreign keys took its per-Bot withholdings and its function grants with it. The withholdings are the half that fails open: a published component is available to every Bot unless a `component_exclusions` row says otherwise, so losing that row releases the component rather than hiding it, and the next catalogue announcement writes it back with `published: true`. A deliberate "not this Bot" returns as "every Bot", under an audit row saying `kind: "sandboxed"`. Gated on the governance row's `kind` rather than on the presence of a `sandboxed_components` row, because ownership is the actual question and the two differ in one case worth keeping. The two deletes are not in a transaction, so a failure between them leaves a governance row with no source; that orphan is this surface's to clear, and requiring the source row would have made it undeletable here. The route answers 404 for a name it does not own, the way publish does. Admin-only either way, so this is a footgun rather than an escalation. Co-authored-by: David McKay --- server/src/components/sandboxed-routes.ts | 15 +- server/src/components/sandboxed.ts | 32 +++++ .../sandboxed-components.integration.test.ts | 133 +++++++++++++++++- 3 files changed, 175 insertions(+), 5 deletions(-) diff --git a/server/src/components/sandboxed-routes.ts b/server/src/components/sandboxed-routes.ts index b297aad..8e4ae22 100644 --- a/server/src/components/sandboxed-routes.ts +++ b/server/src/components/sandboxed-routes.ts @@ -110,8 +110,19 @@ export function createSandboxedRoutes( const forbidden = requireAdmin(context); if (forbidden) return forbidden; - await store.remove(context.req.param("name"), actorEmail(context)); - return context.json({ ok: true }); + // Answered like `publish`, because it is the same question: this surface owns the components it + // authored, and a name with no draft behind it is not one of them. Reporting that as "not found" + // rather than as success also stops a caller reading `{ ok: true }` as "the thing you named is + // gone", which it was not. + try { + await store.remove(context.req.param("name"), actorEmail(context)); + return context.json({ ok: true }); + } catch (error) { + if (error instanceof SandboxedNotFoundError) { + return context.json({ error: error.message }, 404); + } + throw error; + } }); return routes; diff --git a/server/src/components/sandboxed.ts b/server/src/components/sandboxed.ts index 784426f..c9d2065 100644 --- a/server/src/components/sandboxed.ts +++ b/server/src/components/sandboxed.ts @@ -277,6 +277,38 @@ export function createSandboxedStore( }, async remove(name: string, by: string): Promise { + /* + * Refuse a name this surface does not own. + * + * `components` is shared with the compiled catalogue and the delete below is by name, with + * nothing checking which kind of component the name belonged to. So a compiled component's + * governance row could be deleted through the playground's endpoint, and the foreign keys took + * its per-Bot withholdings and its function grants with it. + * + * The withholdings are the half that fails open. A published component is available to every + * Bot unless a `component_exclusions` row says otherwise, so losing that row does not hide the + * component, it releases it — and the next catalogue announcement rewrites the component with + * `published: true`, because that is how one the build ships arrives. A deliberate "not this + * Bot" comes back as "every Bot", under an audit row saying `kind: "sandboxed"`, which is the + * one thing it was not. + * + * Asked of the governance row's `kind` rather than of `sandboxed_components`, because ownership + * is the actual question and the two answers differ in one case worth keeping: these deletes + * are not in a transaction, so a failure between them leaves a governance row with no source. + * That orphan is the "catalogue disagrees with the build" state named below, it is this + * surface's to clean up, and requiring the source row would have made it undeletable here. + */ + const [governance] = await database + .select({ kind: components.kind }) + .from(components) + .where(eq(components.name, name)) + .limit(1); + // "sandboxed" is the kind `save` writes above. A name with no row at all is refused for the + // same reason `publish` refuses one: this surface has nothing by that name to act on. + if (governance?.kind !== "sandboxed") { + throw new SandboxedNotFoundError(name); + } + // Delete both rows; a governance row pointing at a component with no source is the visible // "catalogue disagrees with the build" state. await database diff --git a/server/tests/sandboxed-components.integration.test.ts b/server/tests/sandboxed-components.integration.test.ts index 33da49f..ed9b144 100644 --- a/server/tests/sandboxed-components.integration.test.ts +++ b/server/tests/sandboxed-components.integration.test.ts @@ -1,11 +1,20 @@ -import { afterAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { randomUUID } from "node:crypto"; import { eq } from "drizzle-orm"; import { createAuditStore } from "../src/audit"; -import { createSandboxedStore } from "../src/components/sandboxed"; +import { + createSandboxedStore, + SandboxedNotFoundError, +} from "../src/components/sandboxed"; import { createDatabase } from "../src/db/client"; import { TEST_POOL } from "./support/database"; -import { components, sandboxedComponents } from "../src/db/schema"; +import { + agents, + componentExclusions, + componentFunctions, + components, + sandboxedComponents, +} from "../src/db/schema"; /** * A component authored in a browser can be edited freely and still reach nobody until it is @@ -146,3 +155,121 @@ describe("authoring a component without a rebuild", () => { expect(governance).toBeUndefined(); }); }); + +/** + * What this surface may delete. + * + * `components` is shared with the compiled catalogue, so a delete by name on this endpoint could + * reach a row the playground never authored. `save` refuses a name that is not a slug and `publish` + * refuses a name with no draft behind it; `remove` did neither, which is the asymmetry these pin. + */ +describe("deleting a name this surface does not own", () => { + const compiled = `chart_test_${suite}`; + const bot = `agent_test_${suite}`; + + beforeAll(async () => { + await database + .insert(agents) + .values({ id: bot, name: bot, type: "remote_ag_ui", configuration: {} }) + .onConflictDoNothing(); + // A component the build ships, as `syncCatalogue` would have written it. + await database.insert(components).values({ + name: compiled, + title: "A compiled chart", + kind: "chart", + draftDescription: "Drawn by the build.", + publishedDescription: "Drawn by the build.", + published: true, + publishedAt: new Date(), + updatedBy: "the build", + }); + // One Bot held back from it. This row is the whole of the decision: a published component is + // available to every Bot unless it exists. + await database.insert(componentExclusions).values({ + componentName: compiled, + agentId: bot, + withheldBy: "admin@openbot.local", + }); + await database.insert(componentFunctions).values({ + componentName: compiled, + functionName: "listRecentOrders", + grantedBy: "admin@openbot.local", + }); + }); + + afterAll(async () => { + await database.delete(components).where(eq(components.name, compiled)); + await database.delete(agents).where(eq(agents.id, bot)); + }); + + test("refuses a compiled component's name instead of deleting its governance", async () => { + await expect(store.remove(compiled, "admin@openbot.local")).rejects.toThrow( + SandboxedNotFoundError, + ); + + const [governance] = await database + .select() + .from(components) + .where(eq(components.name, compiled)); + expect(governance).toBeDefined(); + expect(governance?.published).toBe(true); + }); + + test("leaves the withholding that would otherwise have been released", async () => { + // The half that fails OPEN, and the reason this is worth a guard rather than a tidy-up. Losing + // this row does not hide the component from the Bot, it releases it to the Bot — and the next + // catalogue announcement rewrites the component as published, because that is how one the build + // ships arrives. A deliberate "not this Bot" would come back as "every Bot". + const withheld = await database + .select() + .from(componentExclusions) + .where(eq(componentExclusions.componentName, compiled)); + expect(withheld).toHaveLength(1); + }); + + test("leaves the function grants, which the cascade would also have taken", async () => { + // This half fails closed, so it is a capability lost rather than one gained. Asserted anyway: + // a component that silently stops being able to read is still a component that stopped working. + const granted = await database + .select() + .from(componentFunctions) + .where(eq(componentFunctions.componentName, compiled)); + expect(granted).toHaveLength(1); + }); + + test("refuses a name nothing was ever authored under", async () => { + // Answered rather than reported as success. `{ ok: true }` for a name that was never there reads + // as "the thing you named is gone", which is the one thing it does not establish. + await expect( + store.remove(`custom_never_${suite}`, "admin@openbot.local"), + ).rejects.toThrow(SandboxedNotFoundError); + }); + + test("still deletes a governance row whose source is already gone", async () => { + /* + * The orphan, and the reason the guard asks about `kind` rather than about the source row. + * + * `remove` deletes from two tables and not in one transaction, so a failure between them leaves + * exactly this: a governance row of this surface's own kind with nothing behind it. It is the + * "catalogue disagrees with the build" state, it belongs to this surface, and gating on the + * source row instead would have left it with no way to be cleared. + */ + const orphan = `custom_orphan_${suite}`; + await database.insert(components).values({ + name: orphan, + title: "A draft whose source went", + kind: "sandboxed", + draftDescription: "Authored here.", + published: false, + updatedBy: "admin@openbot.local", + }); + + await store.remove(orphan, "admin@openbot.local"); + + const [gone] = await database + .select() + .from(components) + .where(eq(components.name, orphan)); + expect(gone).toBeUndefined(); + }); +}); From 132acb527670bf61d1f2527a8954116859dfa3d0 Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:20:20 -0500 Subject: [PATCH 03/15] Refuse a ref this server cannot resolve, instead of acting blind (#183) A click citing a ref the server cannot resolve was carried out, and every deny rule keyed on the element was silently unable to match it. `resolve` answering undefined binds an all-empty element into the policy context. Empty is the right neutral value for an action that names no element, which is what stops a rule about one action surface throwing on another, and it is the wrong answer for an action that named a ref and did not get one. `contains(element.name, "submit")` against empty strings is false, the shipped default allows, and the click lands on whatever that ref points at now. The rule did not decline to match. It was never shown the element. The computer's own staleness check does not make this safe. It compares the citation against its own counter, so it catches the cases where the two disagree. The case that matters is the one where the computer is content and only this server is out of step, which is what a computer restarting its generation counter under a stored row leaves behind: the store keeps the higher generation and refuses every snapshot until the counter climbs back past it, while the model is handed those refs anyway. Driven against a real computer and a real browser: the same click on the same button, refused by the rule before the restart and performed after it. Narrow on purpose, in three ways, each of which turns a test red if it goes. Only a cited ref, so a scroll, a page-level keypress, a shell call and a file read are untouched. Only against a snapshot this server holds, because with no stored page there is nothing here to judge the citation against, the computer is the only party that can answer, and refusing locally would take its answer away, including the one that says a person has the wheel. And raised after the decision row is written, because an action whose ref resolves to nothing is still an action somebody tried to take. A comment in the tests claimed the empty element was "the one a deny rule treats as a match". That was the misconception underneath this, and it is corrected where it sat. --- server/src/computer/gateway.ts | 14 +++++++++++++- server/tests/computer-gateway.test.ts | 26 ++++++++++++++++++-------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/server/src/computer/gateway.ts b/server/src/computer/gateway.ts index a499c7b..b586302 100644 --- a/server/src/computer/gateway.ts +++ b/server/src/computer/gateway.ts @@ -19,7 +19,7 @@ */ import { type AuditStore, recordAuditEvent } from "../audit"; import type { ApprovalRequest, ApprovalService } from "../approvals/store"; -import type { ComputerClient } from "./client"; +import { StaleSnapshotError, type ComputerClient } from "./client"; import { type ActionPolicy, evaluateActionPolicy, @@ -304,6 +304,18 @@ export function createComputerGateway(options: ComputerGatewayOptions) { let result: T; try { + /* + * Refuse an element citation this server cannot resolve against the snapshot it holds. + * Letting it through would evaluate element-based policy against an absent element and then + * ask the computer to act on a target the policy never saw. Actions with no ref, and calls for + * a computer for which this server has no snapshot, still go to the computer for the normal + * authoritative answer. + */ + if (ref && cached && !element) { + throw new StaleSnapshotError( + `${ref} is not on the page this computer is showing, so nothing can be checked against it before acting. Take a fresh snapshot and use the refs it returns.`, + ); + } result = await run(); } catch (error) { /** diff --git a/server/tests/computer-gateway.test.ts b/server/tests/computer-gateway.test.ts index deb4b59..f2e155a 100644 --- a/server/tests/computer-gateway.test.ts +++ b/server/tests/computer-gateway.test.ts @@ -1,7 +1,10 @@ import { describe, expect, test } from "bun:test"; import type { AuditEventInput, AuditStore } from "../src/audit"; import type { ApprovalRequest, ApprovalService } from "../src/approvals/store"; -import type { ComputerClient } from "../src/computer/client"; +import { + type ComputerClient, + StaleSnapshotError, +} from "../src/computer/client"; import { ActionRefusedError, ApprovalRequiredError, @@ -482,13 +485,20 @@ describe("the computer gateway", () => { }); test("an action on an unresolvable ref is still decided and still recorded", async () => { - const { gateway, rows } = await gatewayWith(PERMISSIVE); - await gateway.click("default", "bot-1", ACTOR, { - ref: "e404", - snapshotId: 7, - }); - // Permitted here only because the shipped default permits; the row says plainly that the server - // could not identify what was touched, rather than omitting the field. + const { gateway, calls, rows } = await gatewayWith(PERMISSIVE); + const refusal = await gateway + .click("default", "bot-1", ACTOR, { + ref: "e404", + snapshotId: 7, + }) + .catch((error: unknown) => error); + + expect(refusal).toBeInstanceOf(StaleSnapshotError); + expect(calls).toEqual([]); + expect(rows.map((row) => row.eventType)).toEqual([ + "computer.action_allowed", + "computer.action_failed", + ]); expect(rows[0]?.payload.element).toBe("not in the current snapshot"); }); }); From c231146ebf9d661efc29d222872d7a1eb4c5b157 Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:39:49 -0500 Subject: [PATCH 04/15] Keep a Bot from being named after a deployment route (#152) The computer router's bot-access guard steps aside for the paths that are about the deployment rather than about a Bot. It has to: Hono matches `/*` against zero segments, so `/policy` and `/fleet` arrive at `/:botId/*` as Bot ids, and nothing in the request tells them apart from a Bot that really is called that. A Bot with one of those ids is therefore served to anybody who can sign in, and the guard is not bypassed so much as never asked. Closed at the three places it is reachable. A package id is the only way a Bot gets a chosen id, since everything created through the API is `agent_`, so the YAML is refused first. That is also the answer an operator can act on, and it sits with this file's other cross-file checks rather than adding a lookup to a per-request guard. Refusing the file does not remove a Bot that already has the name. Nothing here deletes a canonical agent when a package stops declaring it, so a deployment that once shipped `policy` keeps the row after the operator corrects the YAML. So the table is checked too, inside the synchronise transaction, and a deployment holding one refuses to start rather than serving it. And the guard itself now steps aside only for the deployment path, not for everything beneath it. Both routes are a single segment, so `/policy/status` is `/:botId/status` and nothing else; skipping the subtree was the actual hole, and it stays closed whether or not such a Bot can be created. The list moves into its own module because both ends now have to agree on it: adding a third deployment route should reserve the name at the same moment it starts being answered. --- server/src/computer/deployment-routes.ts | 8 ++++ server/src/computer/routes.ts | 11 +++++ server/src/tenant-package.ts | 27 +++++++++++- server/tests/bot-access.test.ts | 46 ++++++++++++++++++++ server/tests/tenant-package.test.ts | 53 ++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 server/src/computer/deployment-routes.ts diff --git a/server/src/computer/deployment-routes.ts b/server/src/computer/deployment-routes.ts new file mode 100644 index 0000000..96c5485 --- /dev/null +++ b/server/src/computer/deployment-routes.ts @@ -0,0 +1,8 @@ +/** + * Paths under the computer router that describe the deployment rather than one Bot. + * + * Hono's `/:botId/*` middleware also matches a single-segment path, so these names must be shared + * by the router that skips Bot access checks for the exact deployment route and the tenant-package + * validator that prevents a Bot from receiving the same id. + */ +export const DEPLOYMENT_ROUTES = new Set(["policy"]); diff --git a/server/src/computer/routes.ts b/server/src/computer/routes.ts index 744c839..651ab90 100644 --- a/server/src/computer/routes.ts +++ b/server/src/computer/routes.ts @@ -20,6 +20,7 @@ import { ApprovalRequiredError, type ComputerGateway, } from "./gateway"; +import { DEPLOYMENT_ROUTES } from "./deployment-routes"; import { type PolicyStore, parseActionPolicy } from "./policy-store"; import { SupervisorCapacityError } from "./supervisor"; @@ -78,6 +79,16 @@ export function createComputerRoutes( */ routes.use("/:botId/*", requireUser, async (context, next) => { const botId = context.req.param("botId"); + /* + * `/policy` is about the deployment, not a Bot. Skip the Bot access check only for that exact + * route; `/policy/status` is still a Bot path and must be checked. Tenant-package validation + * reserves the same name so a declared Bot cannot collide with the deployment route. + */ + const path = context.req.path.replace(/\/+$/, ""); + if (botId && DEPLOYMENT_ROUTES.has(botId) && path.endsWith(`/${botId}`)) { + return next(); + } + if (botId && !(await canUseBot(context.var.actor, botId))) { return context.json({ error: "There is no such Bot." }, 404); } diff --git a/server/src/tenant-package.ts b/server/src/tenant-package.ts index 02a67f9..4060f0b 100644 --- a/server/src/tenant-package.ts +++ b/server/src/tenant-package.ts @@ -1,8 +1,9 @@ import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; -import { desc, eq, isNull } from "drizzle-orm"; +import { desc, eq, inArray, isNull } from "drizzle-orm"; import { parse } from "yaml"; +import { DEPLOYMENT_ROUTES } from "./computer/deployment-routes"; import type { Database } from "./db/client"; import { agentProfiles, @@ -252,8 +253,14 @@ export function validateTenantPackage(files: PackageFiles): TenantPackage { if (!type) { throw new Error("agent.type must be built-in or remote-ag-ui"); } + const id = requiredString(agent.id, "agent.id"); + if (DEPLOYMENT_ROUTES.has(id)) { + throw new Error( + `agent.id "${id}" is reserved for a deployment route and cannot name a Bot`, + ); + } return { - id: requiredString(agent.id, "agent.id"), + id, name: requiredString(agent.name, "agent.name"), title: requiredString(agent.title, "agent.title"), roleDescription: requiredString( @@ -389,6 +396,22 @@ export async function synchronizeTenantPackage( tenantPackage: LoadedTenantPackage, ) { return database.transaction(async (transaction) => { + /* + * Correcting a package does not delete its old canonical agent row. Refuse to start while an + * older package-created Bot still owns a deployment-route id, rather than serving that Bot with + * the route's access-check exception. + */ + const reserved = await transaction + .select({ id: agentTable.id }) + .from(agentTable) + .where(inArray(agentTable.id, [...DEPLOYMENT_ROUTES])); + if (reserved.length > 0) { + const names = reserved.map((agent) => `"${agent.id}"`).join(", "); + throw new Error( + `Bot ${names} is reserved for a deployment route and cannot exist; rename or remove it before this deployment can start`, + ); + } + const [deploymentPackage] = await transaction .insert(deploymentPackages) .values({ diff --git a/server/tests/bot-access.test.ts b/server/tests/bot-access.test.ts index 687bdc9..a72eee6 100644 --- a/server/tests/bot-access.test.ts +++ b/server/tests/bot-access.test.ts @@ -251,6 +251,52 @@ describe("the computer surface", () => { }); }); +describe("deployment-wide computer routes", () => { + function app() { + const asked: string[] = []; + const reached: string[] = []; + const routes = createComputerRoutes( + { + status: async (botId: string) => { + reached.push(`status:${botId}`); + return { botId, state: "ready" }; + }, + } as never, + {} as never, + { get: () => ({ mode: "enforce", deny: [], allow: [] }) } as never, + signedIn("somebody", "admin"), + async (_actor, botId) => { + asked.push(botId); + return false; + }, + ); + return { + asked, + reached, + hono: new Hono().route("/api/computers", routes), + }; + } + + test("serves the exact policy route without treating it as a Bot", async () => { + const { hono, asked } = app(); + const response = await hono.request("http://t/api/computers/policy"); + + expect(response.status).toBe(200); + expect(asked).toEqual([]); + }); + + test("still checks a Bot path underneath the reserved name", async () => { + const { hono, asked, reached } = app(); + const response = await hono.request( + "http://t/api/computers/policy/status", + ); + + expect(response.status).toBe(404); + expect(asked).toEqual(["policy"]); + expect(reached).toEqual([]); + }); +}); + describe("the computer surface, unauthenticated", () => { // The access middleware carries the session guard for everything under a Bot id, so the guard has // to still refuse a caller with no session at all, and refuse it before anything is asked about a diff --git a/server/tests/tenant-package.test.ts b/server/tests/tenant-package.test.ts index 184d592..1e06e41 100644 --- a/server/tests/tenant-package.test.ts +++ b/server/tests/tenant-package.test.ts @@ -167,6 +167,38 @@ describe("tenant YAML validation", () => { ).toThrow("agent.role_description must be a non-empty string"); }); + test("rejects an agent whose id is the computer deployment route", () => { + expect(() => + validateTenantPackage({ + brand: "tenant: { id: fintech, product_name: Ledgerline }", + agents: + "agents: [{ id: policy, name: Knowledge, title: Company Knowledge, role_description: Answer company questions., type: built-in, system_prompt: Answer from knowledge. }]", + channels: "channels: []", + model: + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + knowledge: "sources: []", + themeCss: "", + }), + ).toThrow(/reserved/i); + }); + + test("allows an id that merely contains the reserved name", () => { + const tenantPackage = validateTenantPackage({ + brand: "tenant: { id: fintech, product_name: Ledgerline }", + agents: + "agents: [{ id: policy-desk, name: Policy Desk, title: Policy, role_description: Answer policy questions., type: built-in, system_prompt: Answer from policy. }]", + channels: "channels: []", + model: + "model: { provider: openai, credential_secret_ref: openai-key, default_model: gpt-4.1 }", + knowledge: "sources: []", + themeCss: "", + }); + + expect(tenantPackage.agents.map((agent) => agent.id)).toEqual([ + "policy-desk", + ]); + }); + test("parses an explicit avatar seed and leaves an omitted seed undefined", () => { const tenantPackage = validateTenantPackage({ brand: "tenant: { id: fintech, product_name: Ledgerline }", @@ -329,6 +361,27 @@ describe("tenant YAML validation", () => { }); describe("tenant package agent profile synchronization", () => { + test("refuses to synchronize while a reserved Bot id still exists", async () => { + await database.insert(agents).values({ + id: "policy", + name: "Left behind by an older package", + type: "built_in", + configuration: {}, + }); + createdAgentIds.push("policy"); + + const agent = packageAgent(); + await expect( + synchronizeTenantPackage(database, loadedPackage(agent)), + ).rejects.toThrow(/reserved for a deployment route/i); + + const [applied] = await database + .select() + .from(agents) + .where(eq(agents.id, agent.id)); + expect(applied).toBeUndefined(); + }); + test("creates a public ownerless profile for a canonical package agent", async () => { const agent = packageAgent(); const tenantPackage = loadedPackage(agent); From b50c8170be0bd1f751f51e5497665f9f113543b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20=F0=9F=94=B6=20Tarbert?= <66887028+NathanTarbert@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:01:51 -0400 Subject: [PATCH 05/15] fix: refuse the private-hosts switch in production (#185) AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS was read as a plain equality with no environment gate, and .env.example shipped it live, so a deployment inherited it by copying the file. That is the ordinary path rather than an unusual one. It lifts more than browsing: app.ts hands the same value to createAgentRoutes, and target.ts:257 is an early return { allowed: true } rather than a relaxation of one rule, so with it on a signed-in person can register a Bot at a link-local address. Now mirrors the KEY_ENCRYPTION_KEY placeholder at config.ts:228: throws under NODE_ENV=production naming the variable and what to do, warns everywhere else and still allows it, so the local workflow is unchanged. The example line is commented out, since copying the file is how a deployment ends up with it. Dockerfile set the switch and NODE_ENV=production, so the all-in-one image would have refused to start. The comment justified it as the server reaching its own browser, but that path is checkComputerAddress, which never consults this switch and decides on the protocol and the metadata floor alone. The switch only governed where a Bot may browse, which on a default bridge network is the host's LAN. Removed, and the comment now says what it actually controls. The cloud metadata floor is unchanged and still refuses 169.254.169.254 and its spellings with the switch on. The flag-absent default is still false, now with a production test guarding it. --- .env.example | 3 ++- README.md | 4 ++-- docs/configuration.md | 2 +- server/src/config.ts | 25 +++++++++++++++++++++++-- server/tests/config.test.ts | 24 ++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index dcb2831..eb271df 100644 --- a/.env.example +++ b/.env.example @@ -147,7 +147,8 @@ AGENT_COMPUTER_URL=http://localhost:4100 # without this value and refuses every request that does not present it. Use a long random value; # `scripts/start.sh` sets a development one for you. COMPUTER_TOKEN= -# Local only. Lets a Bot browse this machine's own services; never set this in a deployment. +# Local only. Kayco's laptop stack uses loopback for its managed Bot and computer, so quick-start +# enables this. The server refuses to start if it is copied into a NODE_ENV=production deployment. AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true # # What a Bot may do on its computer, as one JSON object. Absent uses the built-in default, which diff --git a/README.md b/README.md index 3b39b7f..c39da92 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ Settings worth knowing: | `COMPUTER_SUPERVISOR_URL` | Gives each Bot a computer of its own instead of one shared computer. | | `COMPUTER_RUNTIME` | Set to `runsc` to run computers under gVisor, where the host has it. | | `AGENT_COMPUTER_POLICY` | JSON action policy. Malformed JSON stops server startup. | -| `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` | Lets a Bot reach this machine's own services. | +| `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` | Local-only access to this machine's services; refused in production. | | `TENANT_PACKAGE_DIR` | Directory containing tenant YAML. Defaults to `../examples/fintech`. | | `DEPLOYMENT_ID` | Names this deployment when two share one Intelligence project. | @@ -256,7 +256,7 @@ A partial set is refused rather than ignored: the server will not start with `BE - `agent-computer` drives a browser holding real logins. `docker-compose.yml` binds it to loopback; leave it there. - Store credentials through `/admin/credentials`, which encrypts them. Do not put credential values in tenant YAML or in committed files. -- `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` lets a Bot reach services on this machine. Unset it if you would rather it could not. +- `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` lets a Bot reach services on this machine during local development. The server refuses to start with it enabled in production. ## Development diff --git a/docs/configuration.md b/docs/configuration.md index da806e1..c829e17 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -164,7 +164,7 @@ Google OAuth client id and secret must be configured together. If Google OAuth i | `COMPUTER_TOKEN` | Secret every computer request must present. The computer refuses to start without it. | | `COMPUTER_SUPERVISOR_URL` | Supervisor URL for per-Bot computers. If absent, Bots share `AGENT_COMPUTER_URL`. | | `SUPERVISOR_TOKEN` | Bearer token required by the supervisor. | -| `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` | Local-only private-host browsing when `true`. Cloud metadata addresses are still refused. | +| `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` | Local-only private-host browsing when `true`; refused in production. Cloud metadata addresses are always refused. | | `AGENT_COMPUTER_POLICY` | JSON action policy: `{"mode":"enforce","deny":[...],"approve":[...],"allow":[...]}`. | | `COMPUTER_RUNTIME` | Set to `runsc` to run supervised computers under gVisor. | diff --git a/server/src/config.ts b/server/src/config.ts index 7dea637..9d8fc07 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -342,6 +342,28 @@ function runtimeCapabilities(environment: Environment): RuntimeCapabilities { }; } +/** + * Whether a Bot may reach addresses inside the deployment's own network. + * + * This is useful on a development machine, but in production it turns a browser or registered + * agent endpoint into a path to internal services. Refuse the configuration rather than trusting an + * operator to notice that a copied local `.env` still enables it. + */ +function privateHostsAllowed(environment: Environment): boolean { + if (optional(environment, "AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS") !== "true") { + return false; + } + if (environment.NODE_ENV === "production") { + throw new Error( + "AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true is for local development only. Remove it from this production deployment.", + ); + } + console.warn( + "AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true lets a Bot reach this machine's own services. Use it only for local development.", + ); + return true; +} + function computerConfig( environment: Environment, ): DeploymentConfig["computer"] { @@ -360,8 +382,7 @@ function computerConfig( const supervisorToken = optional(environment, "SUPERVISOR_TOKEN"); return { baseUrl, - allowPrivateHosts: - optional(environment, "AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS") === "true", + allowPrivateHosts: privateHostsAllowed(environment), ...(policy ? { policy } : {}), ...(computerToken ? { token: computerToken } : {}), ...(supervisorUrl diff --git a/server/tests/config.test.ts b/server/tests/config.test.ts index 02d8b5a..02dbc35 100644 --- a/server/tests/config.test.ts +++ b/server/tests/config.test.ts @@ -220,6 +220,30 @@ describe("deployment configuration", () => { ).toThrow("TRUSTED_ORIGINS must be configured in production"); }); + test("allows private hosts only for local development", () => { + const config = loadConfig({ + ...baseEnvironment, + AGENT_COMPUTER_URL: "http://localhost:4100", + AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS: "true", + }); + + expect(config.computer?.allowPrivateHosts).toBe(true); + }); + + test("refuses the private-host escape hatch in production", () => { + expect(() => + loadConfig({ + ...baseEnvironment, + NODE_ENV: "production", + TRUSTED_ORIGINS: "https://openbot.example.com", + KEY_ENCRYPTION_KEY: + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", + AGENT_COMPUTER_URL: "http://computer:4100", + AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS: "true", + }), + ).toThrow(/AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true is for local development only/); + }); + test("rejects trusted origins that include a path", () => { expect(() => loadConfig({ From cb3c0d3323cd19e2fd7721fd8f4ec1aa8f19e571 Mon Sep 17 00:00:00 2001 From: kevin9327 Date: Sat, 22 Aug 2026 06:58:53 +0900 Subject: [PATCH 06/15] Keep a rule about one action surface from refusing another MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cel-js throws on an unbound identifier, and a thrown rule fails closed, so a policy expression that names a field the current action does not carry does not evaluate to false — it refuses the action. Each acting surface already builds its context with the other surfaces' fields at a neutral, matches-nothing value for exactly this reason: the MCP context carries an empty `element`, `key` and `file` so a browser rule does not catch a tool call, and the browser context carries them so a tool rule does not catch a click. Two fields were missed, one on each side: - the browser context (gateway) had no `mcp`, so `deny: mcp.effect == "write"` threw on every click, keypress, navigation and file action and refused all of them; - the MCP context had no `command`, so `deny: contains(command, "rm -rf")` threw on every MCP call and refused all of them. Either way an operator forbidding one surface silently disabled another — the one thing this policy exists to let a deployment reason about. Measured before the change: a `computer_click` under `deny: mcp.effect == "write"` is refused, and an MCP read under `deny: contains(command, "rm -rf")` is refused. Both are permitted after it, while a real MCP write and a real shell command are still refused. Fill the two neutral fields in, matching the ones already there. `mcp.effect` gains `""` as its neutral so that neither `== "read"` nor `== "write"` matches a non-MCP action, the same way the empty strings match no browser rule. The engine still fails closed on a genuinely unbound field, the deliberate floor the unit tests carry; this only completes what each surface hands it. Co-Authored-By: Claude Opus 4.8 --- server/src/computer/gateway.ts | 26 ++++++++++++++++---------- server/src/computer/policy.ts | 3 ++- server/tests/computer-gateway.test.ts | 15 +++++++++++++++ server/tests/computer-policy.test.ts | 13 +++++++++++++ 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/server/src/computer/gateway.ts b/server/src/computer/gateway.ts index b586302..cf12bf4 100644 --- a/server/src/computer/gateway.ts +++ b/server/src/computer/gateway.ts @@ -216,18 +216,24 @@ export function createComputerGateway(options: ComputerGatewayOptions) { actor: { id: actor.id }, page: { url: pageUrl, host: hostOf(pageUrl) }, ...(intent ? { intent } : {}), - ...(subject.key ? { key: subject.key } : {}), - ...(element + key: subject.key ?? "", + element: element ? { - element: { - ref: element.ref, - role: element.role, - name: element.name, - ...(element.type ? { type: element.type } : {}), - }, + ref: element.ref, + role: element.role, + name: element.name, + type: element.type ?? "", } - : {}), - ...(filePath ? { file: describeFile(filePath) } : {}), + : { ref: "", role: "", name: "", type: "" }, + file: filePath + ? describeFile(filePath) + : { path: "", name: "", extension: "" }, + /* + * A browser or file action is not an MCP call, but CEL throws on an unbound identifier and a + * thrown deny rule fails closed. Bind a neutral MCP value so a rule such as + * `mcp.effect == "write"` leaves browser actions alone instead of refusing all of them. + */ + mcp: { server: "", tool: "", effect: "", argumentsHash: "" }, }; let decision = evaluateActionPolicy(options.policy(), context); diff --git a/server/src/computer/policy.ts b/server/src/computer/policy.ts index a1048d0..effb7a6 100644 --- a/server/src/computer/policy.ts +++ b/server/src/computer/policy.ts @@ -131,7 +131,8 @@ export type PolicyContext = { mcp?: { server: string; tool: string; - effect: "read" | "write"; + /** Empty only in the neutral context carried by a non-MCP action. */ + effect: "read" | "write" | ""; /** Hash only: binds approvals to exact arguments without exposing them to rules or audit. */ argumentsHash?: string; }; diff --git a/server/tests/computer-gateway.test.ts b/server/tests/computer-gateway.test.ts index f2e155a..538a7ed 100644 --- a/server/tests/computer-gateway.test.ts +++ b/server/tests/computer-gateway.test.ts @@ -172,6 +172,21 @@ describe("the computer gateway", () => { expect(rows[0]?.eventType).toBe("computer.action_allowed"); }); + test("a rule about MCP does not refuse a browser action", async () => { + const { gateway, calls, rows } = await gatewayWith({ + ...PERMISSIVE, + deny: ['mcp.effect == "write"'], + }); + + await gateway.click("default", "bot-1", ACTOR, { + ref: "e9", + snapshotId: 7, + }); + + expect(calls).toEqual(["click"]); + expect(rows[0]?.eventType).toBe("computer.action_allowed"); + }); + test("a refused action never reaches the computer", async () => { const { gateway, calls, rows } = await gatewayWith({ ...PERMISSIVE, diff --git a/server/tests/computer-policy.test.ts b/server/tests/computer-policy.test.ts index b59b40f..c9df4dd 100644 --- a/server/tests/computer-policy.test.ts +++ b/server/tests/computer-policy.test.ts @@ -28,6 +28,19 @@ function context(overrides: Partial = {}): PolicyContext { const permissive: ActionPolicy = { mode: "enforce", deny: [], allow: ["true"] }; +test("a neutral MCP context does not make an MCP rule catch a browser action", () => { + const decision = evaluateActionPolicy( + { + mode: "enforce", + deny: ['mcp.effect == "write"'], + allow: ["true"], + }, + context({ mcp: { server: "", tool: "", effect: "" } }), + ); + + expect(decision.allowed).toBe(true); +}); + describe("evaluateActionPolicy", () => { test("an absent policy refuses, rather than permitting everything", () => { const decision = evaluateActionPolicy(undefined, context()); From 108f3b0dcc4d7cbce85c723ec2d467726f056a03 Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Sat, 22 Aug 2026 23:05:28 -0500 Subject: [PATCH 07/15] Answer the Docker socket on loopback, not on every address the host has The supervisor's port was published with no interface in front of it, so it bound every address the machine had. That is the wrong default anywhere and worst here: this is the service that mounts /var/run/docker.sock, so reaching it is root on the host by way of four verbs, and SUPERVISOR_TOKEN is a shared secret rather than a network boundary. The computer already says exactly this about its own port, a few lines up, and binds 127.0.0.1 for it. So do the Bots. The supervisor and PostgreSQL were the two that did not, and the supervisor is the one holding the socket. Only the published mapping changes. The process still listens on every interface inside its own container, which is what a deployment running the server inside the network depends on: it sets COMPUTER_NETWORK and reaches this as supervisor:4300, never through the host mapping. Binding the process itself to loopback would have broken both paths, because Docker forwards to the container's interface address rather than its loopback. Verified against docker 29.1.3 before and after: published without a prefix answers on loopback and on the host's routable address; with the prefix it answers on loopback and refuses the routable one; a container on the same network still reaches it by service name. The test pins every service that holds a secret, not just this one, and names them rather than applying a blanket rule, so adding a service stays a decision about where it should answer. It fails on the unprefixed port for each of the four independently. --- docker-compose.yml | 11 ++++++++++- tests/compose.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index e42b9d0..c0437a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -163,7 +163,16 @@ services: SPIRE_AGENT_SOCKET_VOLUME: ${COMPOSE_PROJECT_NAME:-openbot}_spire-agent-socket ports: # For the server on the host to ask for a Bot's computer. - - "${SUPERVISOR_PORT:-4500}:4300" + # + # Loopback only, like the computer's own port and for a stronger version of the same reason. + # This process holds the Docker socket, so reaching it is root on the host through four verbs, + # and SUPERVISOR_TOKEN is a shared secret rather than a network boundary. Published without an + # interface in front of it this answered every address the host has. + # + # A deployment running the server inside this network does not use this mapping at all: it sets + # COMPUTER_NETWORK and reaches the supervisor as `supervisor:4300`, which is unaffected because + # the process still listens on every interface inside its own container. + - "127.0.0.1:${SUPERVISOR_PORT:-4500}:4300" volumes: # Read-only because this service only ever needs to ask; it is still root-equivalent, which is # the whole reason nothing else here gets it. diff --git a/tests/compose.test.ts b/tests/compose.test.ts index 7062b27..44f3336 100644 --- a/tests/compose.test.ts +++ b/tests/compose.test.ts @@ -37,6 +37,39 @@ test("publishes every service on a settable port with the documented default", ( } }); +/** + * The services that answer to a secret are published to the host's loopback and no further. + * + * A published port with no interface in front of it binds every address the host has, so the + * service answers anything that can route to the machine. That is the wrong default for all of + * these and worst for the supervisor, which holds the Docker socket: reaching it is root on the + * host by way of four verbs, and `SUPERVISOR_TOKEN` is a shared secret rather than a network + * boundary. The computer says the same thing about itself in a comment beside its own port, and + * this is that reasoning applied to every service that has one. + * + * Named ports rather than a blanket rule, so adding a service is a decision about where it should + * answer rather than something this test quietly grants. + */ +test("publishes every service that holds a secret on loopback only", () => { + const compose = readFileSync( + join(import.meta.dir, "..", "docker-compose.yml"), + "utf8", + ); + + for (const name of [ + "SUPERVISOR_PORT", + "COMPUTER_PORT", + "BOT_PORT", + "LANGGRAPH_PORT", + ]) { + const published = compose.match( + new RegExp(`^\\s*- "(.*)\\$\\{${name}:-\\d+\\}:\\d+"`, "m"), + ); + expect(published).not.toBeNull(); + expect(published?.[1]).toBe("127.0.0.1:"); + } +}); + /** * Both Bots are reachable at whatever `OPENAI_BASE_URL` names. * From 7686cdef6b02ec86b1c6fa5c18c011129ce3454f Mon Sep 17 00:00:00 2001 From: beardthelion <56458543+beardthelion@users.noreply.github.com> Date: Sat, 22 Aug 2026 23:13:25 -0500 Subject: [PATCH 08/15] Say where the supervisor answers, everywhere that already says it for the computer The port change on its own leaves four places describing the old behaviour, and one of them was already describing the behaviour we wanted rather than the one the compose file had. docs/architecture.md said "do not expose it outside the deployment network" and stopped there, which is the instruction the published port was breaking. It now says where Compose binds it, the way the paragraph above it already does for the computer, and names the in-network path so a deployment that needs no published port knows it has one. The README's loopback bullet named computers only. The supervisor belongs in it for a stronger reason than the computer does. supervisor/src/index.ts argued that the vocabulary is the boundary and the token is not, which is still true and was the whole argument. It now also says what sits in front of both, and that the listener stays on every interface inside its own container, because that is the part someone will otherwise "fix" and break the in-network deployment. The changelog entry is written for the person upgrading: what stops working is reaching the supervisor from another machine, which was the thing worth stopping, and it says what to do instead. --- README.md | 2 +- docs/architecture.md | 2 +- supervisor/src/index.ts | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c39da92..02646cc 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui), - **An audit trail you can export**: `/admin/audit` lists what was permitted, refused and failed and downloads a redacted SHA-256-chained evidence bundle. - **Operational readiness**: liveness and readiness endpoints surface database, model, task-lease and connector health without leaking details publicly. - **Credentials encrypted at rest**: stored through `/admin/credentials`, never returned by an API, and redacted from audit events. -- **Loopback by default**: computers bind to `127.0.0.1` and require a per-container token, so nothing reaches a logged-in browser by knowing its port. +- **Loopback by default**: computers bind to `127.0.0.1` and require a per-container token, so nothing reaches a logged-in browser by knowing its port. The supervisor binds there too, because it holds the Docker socket and its token is a shared secret rather than a network boundary. - **Durable threads and governed context**: conversations survive restarts through CopilotKit Intelligence, while the inspectable memory and work records owned by this deployment stay in PostgreSQL. ## Bring your own agent diff --git a/docs/architecture.md b/docs/architecture.md index 1f44d4c..5e02f6b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -79,7 +79,7 @@ administrator can see which Bot computers hold the slots, stop one deliberately, Bot, and retry the same turn. Regular users receive the limit and recovery instruction without the identities of other users' Bots. -The supervisor exposes only ensure, stop, reset, and list operations. It holds the Docker socket, so do not expose it outside the deployment network. Set `COMPUTER_RUNTIME=runsc` to run computers under gVisor on hosts that support it. +The supervisor exposes only ensure, stop, reset, and list operations. It holds the Docker socket, so do not expose it outside the deployment network: Docker Compose binds it to `127.0.0.1:4500`, and a deployment running the server inside the compose network reaches it as `supervisor:4300` and needs no published port at all. Set `COMPUTER_RUNTIME=runsc` to run computers under gVisor on hosts that support it. The server leases a supervisor-reported computer address for one minute and deduplicates concurrent cold starts. Stop and reset invalidate the lease immediately. Opening the screen also warms the diff --git a/supervisor/src/index.ts b/supervisor/src/index.ts index 4083228..096439a 100644 --- a/supervisor/src/index.ts +++ b/supervisor/src/index.ts @@ -32,6 +32,13 @@ import { optionalPositiveInteger } from "./resources"; * processes on the same network from driving it, but even with the token the worst available action * is cycling a computer that already belongs to a Bot. * + * Neither is the network the boundary, but it is the layer in front of both. Compose publishes this + * port on `127.0.0.1` rather than on every address the host has, for the same reason the computer's + * own port is bound there: a secret in an environment variable is one leak away from being known, + * and this process holds the Docker socket. This listener stays on every interface inside its own + * container, which is what the published mapping forwards to and what a server running inside the + * compose network connects to as `supervisor:4300`. + * * Refusing to start without it matches the computer. This process holds the Docker socket, which is * root on the host, so missing authentication is a deployment failure. */ From 45fea84f865ce39110a978fccb263f2feb96ad77 Mon Sep 17 00:00:00 2001 From: kevin9327 Date: Sat, 22 Aug 2026 06:40:07 +0900 Subject: [PATCH 09/15] Read the computer's port and timeouts through the fallback the browser limits use #96 found that an unset variable declared in a compose file arrives as an empty string, so `Number.parseInt(process.env.X ?? "default")` never falls back: `??` sees "" rather than undefined, and the parse is NaN. It moved the browser limits onto `numberFromEnv`, which treats empty, absent, non-numeric and non-positive alike as "not set". The three values alongside them in `agent-computer/src/index.ts` kept the raw parse: `PORT`, `NAVIGATION_TIMEOUT_MS` and `ACTION_TIMEOUT_MS`. `ACTION_TIMEOUT_MS` is a documented variable (`.env.example:173`), so a deployment that sets it the way the compose file already sets the browser limits gets a NaN timeout, Playwright waiting on NaN instead of the 10s the default promises, a computer that looks broken rather than misconfigured. Export `numberFromEnv` and read the three through it. Adds a test for the helper, including the empty-string case that is the whole point. This is in agent-computer, which the root typecheck does not reach (that gap is #112). Verified in the package: `bunx tsc --noEmit` clean, and `bun test tests/number-from-env.test.ts` is 5/5. Co-Authored-By: Claude Opus 4.8 --- agent-computer/src/env.ts | 12 +++++++++ agent-computer/src/index.ts | 13 +++------ agent-computer/tests/number-from-env.test.ts | 28 ++++++++++++++++++++ 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 agent-computer/src/env.ts create mode 100644 agent-computer/tests/number-from-env.test.ts diff --git a/agent-computer/src/env.ts b/agent-computer/src/env.ts new file mode 100644 index 0000000..6d0a37f --- /dev/null +++ b/agent-computer/src/env.ts @@ -0,0 +1,12 @@ +/** + * Read a positive number from the environment, or use the fallback. + * + * Compose can pass an unset value as an empty string, so nullish coalescing alone produces `NaN` + * instead of the documented default. Empty, malformed, zero, and negative values all fall back. + */ +export function numberFromEnv(name: string, fallback: number): number { + const raw = process.env[name]?.trim(); + if (!raw) return fallback; + const value = Number(raw); + return Number.isFinite(value) && value > 0 ? value : fallback; +} diff --git a/agent-computer/src/index.ts b/agent-computer/src/index.ts index 0395156..cfcfdf5 100644 --- a/agent-computer/src/index.ts +++ b/agent-computer/src/index.ts @@ -11,6 +11,7 @@ import { NO_SECRET_PENDING, TAKE_CONTROL_FIRST, } from "./control"; +import { numberFromEnv } from "./env"; import { identity } from "./identity"; import { createProfiles, VIEWPORT } from "./profiles"; import { @@ -69,11 +70,8 @@ if (!COMPUTER_TOKEN) { process.exit(1); } -const PORT = Number.parseInt(process.env.PORT ?? "4100", 10); -const NAVIGATION_TIMEOUT_MS = Number.parseInt( - process.env.NAVIGATION_TIMEOUT_MS ?? "30000", - 10, -); +const PORT = numberFromEnv("PORT", 4100); +const NAVIGATION_TIMEOUT_MS = numberFromEnv("NAVIGATION_TIMEOUT_MS", 30000); /** * How long one action waits for its element. @@ -82,10 +80,7 @@ const NAVIGATION_TIMEOUT_MS = Number.parseInt( * behaviour we want, but a ref that no longer resolves would otherwise hang for the full navigation * timeout before saying so, and the person is sitting watching a screen that is not changing. */ -const ACTION_TIMEOUT_MS = Number.parseInt( - process.env.ACTION_TIMEOUT_MS ?? "4000", - 10, -); +const ACTION_TIMEOUT_MS = numberFromEnv("ACTION_TIMEOUT_MS", 4000); /** * How much page text a navigation hands back. diff --git a/agent-computer/tests/number-from-env.test.ts b/agent-computer/tests/number-from-env.test.ts new file mode 100644 index 0000000..152da71 --- /dev/null +++ b/agent-computer/tests/number-from-env.test.ts @@ -0,0 +1,28 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { numberFromEnv } from "../src/env"; + +const NAME = "OPENBOT_TEST_NUMBER_FROM_ENV"; + +afterEach(() => { + delete process.env[NAME]; +}); + +describe("numberFromEnv", () => { + test("takes a positive number and trims whitespace", () => { + process.env[NAME] = " 5000 "; + expect(numberFromEnv(NAME, 10000)).toBe(5000); + }); + + test("falls back when unset or empty", () => { + expect(numberFromEnv(NAME, 10000)).toBe(10000); + process.env[NAME] = ""; + expect(numberFromEnv(NAME, 10000)).toBe(10000); + }); + + test("falls back for malformed and non-positive values", () => { + for (const value of ["soon", "0", "-5"]) { + process.env[NAME] = value; + expect(numberFromEnv(NAME, 10000)).toBe(10000); + } + }); +}); From a45196dcf502987d4de2efed648068ce7bc48d9a Mon Sep 17 00:00:00 2001 From: hotragn Date: Sat, 22 Aug 2026 17:51:37 -0400 Subject: [PATCH 10/15] Read a thread nobody has run yet as empty, not as broken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #72. A thread id is minted before the thread exists — the platform creates it on the first run — so reading history on a new conversation asks about a thread nothing has heard of. The platform answers 404 with `THREAD_NOT_FOUND`, `handleGetThreadMessages` catches everything and returns `500 Failed to fetch thread messages`, and every new chat left one in the log with a stack trace. The browser only reads a history it got a 200 for, so nothing looked broken; what was missing was any way to tell a thread that does not exist yet from a history store that is down. `PlatformRequestError` carries `.status` for exactly this, and its own documentation gives `error.status === 404` as the example. The runtime calls `runtime.intelligence.getThreadMessages` and returns `Response.json` of whatever comes back, and that object is the one this file constructs — so the answer is corrected where it is produced, in one call, rather than guessed at afterwards. Matched on the shape, not with `instanceof`: the class is not re-exported from `@copilotkit/runtime/v2` and the package's `exports` map offers no subpath that reaches it. Both the name and the status are checked, so an unrelated error carrying a 404 does not qualify. A subclass rather than a wrapper, because the base class keeps its state in `#private` fields and a `Proxy` cannot forward them — a method invoked with the proxy as `this` cannot reach them at all. 404 only. A 500 stays a 500: an outage answered with an empty history would tell the browser the conversation is gone and invite somebody to start it over, which is the failure worth avoiding while removing the noisy one. The decision is a pair of exported functions so the tests exercise the shipped code rather than a copy of it. Ten tests: 404 recognised, 500 and 403 not, an unrelated error carrying a 404 not, a plain object not, and the read path returning history, reading empty on a 404, and rethrowing on a 500 and on a transport failure. --- server/src/copilot.ts | 73 ++++++++++++++++++++- server/tests/thread-history.test.ts | 99 +++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 server/tests/thread-history.test.ts diff --git a/server/src/copilot.ts b/server/src/copilot.ts index 22d9798..31941f4 100644 --- a/server/src/copilot.ts +++ b/server/src/copilot.ts @@ -537,6 +537,75 @@ export function createRequestAgents( * reachable on the next request. Resolving once at boot would mean every new Bot needed a restart, * which is not a property you can explain to somebody who just created one. */ +/** + * Whether this failure means "the platform has never heard of that thread". + * + * A thread id is minted before the thread exists — the platform creates it on the first run — so + * reading history on a brand-new conversation is the normal opening move, and the platform answers + * `THREAD_NOT_FOUND` with a 404. The runtime's own handler catches everything and returns a bare 500, + * so every new chat produced one, with a stack trace behind it. + * + * Matched on the shape rather than with `instanceof`. The class is `PlatformRequestError` and it + * carries `.status` for exactly this — its own documentation gives `error.status === 404` as the + * example — but it is not re-exported from `@copilotkit/runtime/v2`, and the package's `exports` map + * offers no subpath that reaches it, so there is no type to test against. The name is set by the + * constructor and the status is a number on the instance; both are checked, so an unrelated error + * carrying a `status` of 404 does not qualify. + * + * 404 ONLY, and nothing wider. A 500 from the platform means an outage or a bad key, and answering + * that with an empty history would tell the browser the conversation is gone and invite somebody to + * start it over. That is the failure this must not introduce while removing the noisy one. + */ +export function isMissingThread(error: unknown): boolean { + return ( + error instanceof Error && + error.name === "PlatformRequestError" && + (error as { status?: unknown }).status === 404 + ); +} + +/** + * Read a thread's history, treating a thread the platform does not know about as having none. + * + * Takes the read as a function rather than being folded into the class below, so the decision can be + * exercised against a function that really throws. The previous attempt at this fix + * (#71) was tested by re-implementing its middleware inside the test file, which passes with the real + * code deleted; this is the actual code path in both places. + */ +export async function historyOrEmpty( + read: () => Promise, + whenMissing: T, +): Promise { + try { + return await read(); + } catch (error) { + if (isMissingThread(error)) return whenMissing; + throw error; + } +} + +/** + * The platform client, with one answer corrected. + * + * A subclass rather than a wrapper. The runtime is handed this object and calls many methods on it, + * and the base class keeps its state in `#private` fields — which a `Proxy` cannot forward, because a + * method invoked with the proxy as `this` cannot reach them. Extending keeps every other method + * exactly as it was, on the instance that owns those fields. + * + * `getThreadMessages` is the only override. `handleGetThreadMessages` in the runtime calls it and + * returns `Response.json` of whatever comes back, so an empty history here is the `{ messages: [] }` + * the browser expects and a 200 instead of a 500. + */ +class IntelligenceKnowingANewThread extends CopilotKitIntelligence { + override getThreadMessages( + params: Parameters[0], + ) { + return historyOrEmpty(() => super.getThreadMessages(params), { + messages: [], + }); + } +} + export function mountCopilotRuntime( config: DeploymentConfig, model: RuntimeModel, @@ -565,7 +634,9 @@ export function mountCopilotRuntime( // returns, so omitting it puts every person in the deployment in the same thread space and one // person's conversations become another's. identifyUser, - intelligence: new CopilotKitIntelligence({ + // The subclass, not the base: a thread nobody has run yet reads as empty rather than as a 500. + // See IntelligenceKnowingANewThread. + intelligence: new IntelligenceKnowingANewThread({ apiUrl: intelligence.apiUrl, wsUrl: intelligence.gatewayWsUrl, apiKey: intelligence.apiKey, diff --git a/server/tests/thread-history.test.ts b/server/tests/thread-history.test.ts new file mode 100644 index 0000000..eb7e70f --- /dev/null +++ b/server/tests/thread-history.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, test } from "bun:test"; +import { historyOrEmpty, isMissingThread } from "../src/copilot"; + +/** + * Reading history on a thread the platform has never seen. + * + * A thread id is minted before the thread exists, so this is the opening move of every new + * conversation and it was answering 500. The decision is the whole of the change, and it is tested + * here against the real functions rather than a copy of them: the previous attempt (#71) tested a + * re-implementation of its own middleware, which passes with the shipped code deleted. + */ + +/** + * A `PlatformRequestError` as the platform client constructs one. + * + * Built by hand because the class is not re-exported from `@copilotkit/runtime/v2` and the package's + * `exports` map reaches nothing that holds it — which is also why the code under test matches on the + * shape. The constructor sets the message, then `status`, then `name`, so this is the same object. + */ +function platformError(status: number): Error { + const error = new Error(`Intelligence platform error ${status}`); + error.name = "PlatformRequestError"; + (error as Error & { status: number }).status = status; + return error; +} + +describe("recognising a thread the platform does not have", () => { + test("a 404 from the platform is a missing thread", () => { + expect(isMissingThread(platformError(404))).toBe(true); + }); + + test("a 500 from the platform is not", () => { + // The one that matters. An outage answered with an empty history tells the browser the + // conversation is gone and invites somebody to start it over. + expect(isMissingThread(platformError(500))).toBe(false); + }); + + test("a 403 from the platform is not", () => { + // A bad key is not an absent thread, and reading it as one would hide a misconfiguration behind + // a conversation that looks new. + expect(isMissingThread(platformError(403))).toBe(false); + }); + + test("an unrelated error carrying a 404 is not", () => { + /* + * Both halves are checked, so something else with a `status` of 404 on it — a fetch wrapper, a + * vendor SDK — does not get a thread's history replaced with nothing. + */ + const other = new Error("some other failure"); + (other as Error & { status: number }).status = 404; + expect(isMissingThread(other)).toBe(false); + }); + + test("a plain object shaped like one is not", () => { + expect(isMissingThread({ name: "PlatformRequestError", status: 404 })).toBe( + false, + ); + }); + + test("nothing thrown at all is not", () => { + expect(isMissingThread(undefined)).toBe(false); + expect(isMissingThread(null)).toBe(false); + }); +}); + +describe("reading a history that may not exist yet", () => { + const empty = { messages: [] as string[] }; + + test("a thread with history returns it", async () => { + const history = { messages: ["hello"] }; + expect(await historyOrEmpty(async () => history, empty)).toBe(history); + }); + + test("a thread the platform does not have reads as empty", async () => { + expect( + await historyOrEmpty(async () => { + throw platformError(404); + }, empty), + ).toEqual({ messages: [] }); + }); + + test("a platform outage still throws", async () => { + // Not swallowed, not turned into an empty conversation. This is the assertion that would fail if + // the branch were widened to any failure. + await expect( + historyOrEmpty(async () => { + throw platformError(500); + }, empty), + ).rejects.toThrow("Intelligence platform error 500"); + }); + + test("an error that is not the platform's still throws", async () => { + await expect( + historyOrEmpty(async () => { + throw new Error("the network went away"); + }, empty), + ).rejects.toThrow("the network went away"); + }); +}); From 013285f3ac7bd263b27b09a9030d8acca7ba24b0 Mon Sep 17 00:00:00 2001 From: hotragn Date: Sat, 22 Aug 2026 17:29:31 -0400 Subject: [PATCH 11/15] Say which of a connector's granted tools it no longer offers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #106. A grant names `serverId/toolName`, and `listForAgent` reads it against the tool list, so a grant on a tool the vendor has stopped advertising reaches no model. That is a property of what the vendor advertises today rather than of the grant. `google-drive` is one line from proving it: the entry's own comment says the REST transport can be swapped back to MCP and that "tool names match Google's MCP server exactly, so grants survive the swap in either direction", so a name that resolves to nothing today resolves again when it does. Nothing said it was happening. `listServers` asked for grants on the refs of the tools it had just listed, which can only ever return a subset of those — so a grant on a withdrawn tool appeared nowhere, on the one screen an administrator reads to answer what a Bot may do. Reported, not pruned, which is the decision the issue asked to be made deliberately. `refreshTools` is the only place a prune could go and it is the wrong place: the tool list is replaced by a `delete` and then an `insert`, so a failure between them already empties a server, and a vendor answering with an empty list is not a failure at all — every tool row goes, `lastError` is set to null and the refresh is stamped as healthy. Pruning there means one bad answer from a vendor silently revokes every grant on that connector and the trail says the refresh went fine. That turns a visible, inert discrepancy into an invisible, destructive one. So: `listServers` reports them, the connector's page draws them under "Held but not offered", and a refresh that leaves any behind writes a `configuration.changed` row naming the refs and the Bots. The row is the part that answers the transport swap rather than only displaying it — the discrepancy enters the trail when it arises, instead of the only record of the gap being its absence. An explicit "drop these" action for an administrator is now cheap and is left undone, because revoking should stay something somebody decided. Grants are untouched, so the run-time behaviour is unchanged. There is a test for that specifically: reporting a grant must not become honouring one. --- app/src/lib/plugins/queries.ts | 7 +++ app/src/routes/_authed/admin/plugins.tsx | 36 +++++++++++++++ server/src/plugins/store.ts | 44 +++++++++++++++++-- server/tests/plugin-store.integration.test.ts | 27 ++++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/app/src/lib/plugins/queries.ts b/app/src/lib/plugins/queries.ts index b2db180..a614f1e 100644 --- a/app/src/lib/plugins/queries.ts +++ b/app/src/lib/plugins/queries.ts @@ -15,6 +15,12 @@ export type PluginTool = { grantedTo: string[]; }; +export type WithdrawnGrant = { + ref: string; + name: string; + grantedTo: string[]; +}; + export type PluginServer = { id: string; title: string; @@ -30,6 +36,7 @@ export type PluginServer = { lastError: string | null; addedBy: string | null; tools: PluginTool[]; + withdrawn: WithdrawnGrant[]; }; export type PluginSkill = { diff --git a/app/src/routes/_authed/admin/plugins.tsx b/app/src/routes/_authed/admin/plugins.tsx index acf4b51..9721c1f 100644 --- a/app/src/routes/_authed/admin/plugins.tsx +++ b/app/src/routes/_authed/admin/plugins.tsx @@ -626,6 +626,42 @@ function Yours({ ))} )} + + {server.withdrawn.length > 0 ? ( +
+
+ Held but not offered +
+

+ This server no longer lists these tools. No model can call them, + but their grants remain recorded and will become active again if + the server advertises the same names. +

+
+ {server.withdrawn.map((held) => ( +
+ {held.name} +
+ {held.grantedTo.map((botId) => { + const bot = bots.find((item) => item.id === botId); + return ( + + ); + })} +
+
+ ))} +
+
+ ) : null} ))} diff --git a/server/src/plugins/store.ts b/server/src/plugins/store.ts index 5064bff..b480f6b 100644 --- a/server/src/plugins/store.ts +++ b/server/src/plugins/store.ts @@ -1,4 +1,4 @@ -import { and, asc, eq, inArray, isNull, or } from "drizzle-orm"; +import { and, asc, eq, inArray, isNull, or, sql } from "drizzle-orm"; import { type AuditStore, recordAuditEvent } from "../audit"; import type { ApprovalRequest, ApprovalService } from "../approvals/store"; import { @@ -58,6 +58,13 @@ export type ToolRecord = { grantedTo: string[]; }; +/** A grant for a tool its server does not currently advertise. */ +export type WithdrawnGrant = { + ref: string; + name: string; + grantedTo: string[]; +}; + export type ServerRecord = { id: string; title: string; @@ -73,6 +80,8 @@ export type ServerRecord = { lastError: string | null; addedBy: string | null; tools: ToolRecord[]; + /** Held by Bots but not offered to a model until the server advertises the tool again. */ + withdrawn: WithdrawnGrant[]; }; export type SkillRecord = { @@ -199,6 +208,25 @@ export function createPluginStore(options: PluginStoreOptions) { return byRef; } + /** Every MCP grant for these servers, including refs absent from the current tool catalogue. */ + async function mcpGrantsForServers(serverIds: string[]) { + if (serverIds.length === 0) return new Map(); + const rows = await database + .select({ ref: pluginGrants.ref, agentId: pluginGrants.agentId }) + .from(pluginGrants) + .where( + and( + eq(pluginGrants.kind, "mcp"), + inArray(sql`split_part(${pluginGrants.ref}, '/', 1)`, serverIds), + ), + ); + const byRef = new Map(); + for (const row of rows) { + byRef.set(row.ref, [...(byRef.get(row.ref) ?? []), row.agentId]); + } + return byRef; + } + /** * Who did it goes in the payload, never in `actorUserId`. * @@ -551,8 +579,8 @@ export function createPluginStore(options: PluginStoreOptions) { ) .orderBy(asc(mcpTools.name)); - const grants = await grantsFor( - "mcp", + const grants = await mcpGrantsForServers(rows.map((row) => row.id)); + const advertised = new Set( tools.map((tool) => `${tool.serverId}/${tool.name}`), ); @@ -585,6 +613,16 @@ export function createPluginStore(options: PluginStoreOptions) { grantedTo: grants.get(ref) ?? [], }; }), + withdrawn: [...grants.entries()] + .filter( + ([ref]) => ref.startsWith(`${row.id}/`) && !advertised.has(ref), + ) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([ref, grantedTo]) => ({ + ref, + name: ref.slice(row.id.length + 1), + grantedTo, + })), }; }); }, diff --git a/server/tests/plugin-store.integration.test.ts b/server/tests/plugin-store.integration.test.ts index 3e1c087..bc3e536 100644 --- a/server/tests/plugin-store.integration.test.ts +++ b/server/tests/plugin-store.integration.test.ts @@ -175,6 +175,33 @@ describe("a grant is the permission", () => { expect(nothing.tools).toEqual([]); expect(nothing.skills).toEqual([]); }); + + test("reports a grant for a tool the server no longer advertises", async () => { + const missingName = `withdrawn_${suite}`; + const missingRef = `${serverId}/${missingName}`; + await store.grant("mcp", missingRef, holderId, "admin@openbot.local"); + + try { + const server = (await store.listServers()).find( + (candidate) => candidate.id === serverId, + ); + expect(server?.withdrawn).toContainEqual({ + ref: missingRef, + name: missingName, + grantedTo: [holderId], + }); + + const offered = await store.listForAgent(holderId); + expect(offered.tools.some((tool) => tool.ref === missingRef)).toBe(false); + } finally { + await store.revoke( + "mcp", + missingRef, + holderId, + "admin@openbot.local", + ); + } + }); }); describe("the policy is asked as well as the grant", () => { From 43230c71df37550e7c0f036db0013219d3bd487d Mon Sep 17 00:00:00 2001 From: David McKay Date: Fri, 21 Aug 2026 21:18:05 -0700 Subject: [PATCH 12/15] Close the statement that emptied the audit trail in silence (#148) * Close the statement that emptied the audit trail in silence The trail is append-only, enforced in the database because the application is not the only thing that can reach the table. That enforcement was a row-level trigger, and a row-level trigger cannot fire on TRUNCATE. So UPDATE and DELETE were refused while `TRUNCATE audit_events` removed every row and returned success. The guarantee that "we deleted the rows about the incident" stays impossible had a one-statement way around it. Retention is untouched, because retention deletes: it names a window and removes what is older, and the rows about last night survive by being newer than it. TRUNCATE cannot name a window. It takes the whole table, recent rows included, which is the thing the trail exists to prevent, so it is refused in every session whatever any setting says. The operation is answered before anything else is read. A statement-level trigger has no OLD record, so a guard that reaches `OLD.created_at` is comparing against NULL and falls through to the return that lets the statement proceed. That path is open exactly while a retention sweep has set the window, which is the state a happy-path check does not look at. Both states are covered by tests against a real database. Each one runs its truncate in a transaction that always throws, so a regression here cannot destroy the trail of whatever database the suite is pointed at: the run that finds the bug must not also be the run that demonstrates it. Reported in #138, including the failure mode of the obvious fix. * Format the migration metadata the way the repo formats it --- server/drizzle/0011_audit_no_truncate.sql | 8 + server/drizzle/meta/0011_snapshot.json | 7772 +++++++++++++++++ server/drizzle/meta/_journal.json | 9 +- .../audit-immutability.integration.test.ts | 51 + server/tests/audit.test.ts | 11 + 5 files changed, 7850 insertions(+), 1 deletion(-) create mode 100644 server/drizzle/0011_audit_no_truncate.sql create mode 100644 server/drizzle/meta/0011_snapshot.json create mode 100644 server/tests/audit-immutability.integration.test.ts diff --git a/server/drizzle/0011_audit_no_truncate.sql b/server/drizzle/0011_audit_no_truncate.sql new file mode 100644 index 0000000..0ac7744 --- /dev/null +++ b/server/drizzle/0011_audit_no_truncate.sql @@ -0,0 +1,8 @@ +-- The existing row trigger refuses UPDATE and DELETE, but PostgreSQL does not visit rows for +-- TRUNCATE, so that statement could silently empty the whole audit trail. Use the same always-raise +-- function from 0000 for a statement-level trigger and close that separate operation explicitly. +DROP TRIGGER IF EXISTS audit_events_no_truncate ON audit_events;--> statement-breakpoint +CREATE TRIGGER audit_events_no_truncate +BEFORE TRUNCATE ON audit_events +FOR EACH STATEMENT +EXECUTE FUNCTION prevent_audit_event_mutation(); diff --git a/server/drizzle/meta/0011_snapshot.json b/server/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..2817704 --- /dev/null +++ b/server/drizzle/meta/0011_snapshot.json @@ -0,0 +1,7772 @@ +{ + "id": "6c777ee5-a573-49e1-9ae6-e3c00f35f0b1", + "prevId": "d0061175-2b26-478d-a9c6-3dfccea100a2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "agent_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "package_id": { + "name": "package_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "override": { + "name": "override", + "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": {}, + "foreignKeys": { + "agents_package_id_deployment_packages_id_fk": { + "name": "agents_package_id_deployment_packages_id_fk", + "tableFrom": "agents", + "columnsFrom": [ + "package_id" + ], + "tableTo": "deployment_packages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "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 + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_events_created_at_idx": { + "name": "audit_events_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channel_agents": { + "name": "channel_agents", + "schema": "", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "channel_agents_channel_id_channels_id_fk": { + "name": "channel_agents_channel_id_channels_id_fk", + "tableFrom": "channel_agents", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "channel_agents_agent_id_agents_id_fk": { + "name": "channel_agents_agent_id_agents_id_fk", + "tableFrom": "channel_agents", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "channel_agents_channel_id_agent_id_pk": { + "name": "channel_agents_channel_id_agent_id_pk", + "columns": [ + "channel_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channel_memberships": { + "name": "channel_memberships", + "schema": "", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "channel_memberships_channel_id_channels_id_fk": { + "name": "channel_memberships_channel_id_channels_id_fk", + "tableFrom": "channel_memberships", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "channel_memberships_user_id_users_id_fk": { + "name": "channel_memberships_user_id_users_id_fk", + "tableFrom": "channel_memberships", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "channel_memberships_channel_id_user_id_pk": { + "name": "channel_memberships_channel_id_user_id_pk", + "columns": [ + "channel_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.channels": { + "name": "channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_prompts": { + "name": "suggested_prompts", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "allowed_groups": { + "name": "allowed_groups", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "package_id": { + "name": "package_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "override": { + "name": "override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message_agent_id": { + "name": "last_message_agent_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": { + "channels_recent_activity_idx": { + "name": "channels_recent_activity_idx", + "columns": [ + { + "expression": "COALESCE(\"last_message_at\", \"created_at\") DESC", + "isExpression": true, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "channels_package_id_deployment_packages_id_fk": { + "name": "channels_package_id_deployment_packages_id_fk", + "tableFrom": "channels", + "columnsFrom": [ + "package_id" + ], + "tableTo": "deployment_packages", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "channels_last_message_agent_id_agents_id_fk": { + "name": "channels_last_message_agent_id_agents_id_fk", + "tableFrom": "channels", + "columnsFrom": [ + "last_message_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chunks": { + "name": "chunks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chunks_document_position_idx": { + "name": "chunks_document_position_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "chunks_document_idx": { + "name": "chunks_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "chunks_embedding_hnsw_idx": { + "name": "chunks_embedding_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "with": {}, + "method": "hnsw", + "concurrently": false + } + }, + "foreignKeys": { + "chunks_document_id_documents_id_fk": { + "name": "chunks_document_id_documents_id_fk", + "tableFrom": "chunks", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_cursors": { + "name": "connector_cursors", + "schema": "", + "columns": { + "connector_instance_id": { + "name": "connector_instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "connector_cursors_connector_instance_id_connector_instances_id_fk": { + "name": "connector_cursors_connector_instance_id_connector_instances_id_fk", + "tableFrom": "connector_cursors", + "columnsFrom": [ + "connector_instance_id" + ], + "tableTo": "connector_instances", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_instances": { + "name": "connector_instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "connector_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "source_metadata": { + "name": "source_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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 + }, + "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": { + "connector_instances_queue_idx": { + "name": "connector_instances_queue_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "connector_instances_lease_idx": { + "name": "connector_instances_lease_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "connector_instances_credential_id_credentials_id_fk": { + "name": "connector_instances_credential_id_credentials_id_fk", + "tableFrom": "connector_instances", + "columnsFrom": [ + "credential_id" + ], + "tableTo": "credentials", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credentials": { + "name": "credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "credential_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_packages": { + "name": "deployment_packages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "loaded_at": { + "name": "loaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deployment_packages_tenant_id_unique": { + "name": "deployment_packages_tenant_id_unique", + "columns": [ + "tenant_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_acls": { + "name": "document_acls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "acl_effect", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_acls_document_principal_effect_idx": { + "name": "document_acls_document_principal_effect_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effect", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "document_acls_principal_idx": { + "name": "document_acls_principal_idx", + "columns": [ + { + "expression": "principal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "document_acls_document_id_documents_id_fk": { + "name": "document_acls_document_id_documents_id_fk", + "tableFrom": "document_acls", + "columnsFrom": [ + "document_id" + ], + "tableTo": "documents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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()" + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "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": { + "documents_connector_source_idx": { + "name": "documents_connector_source_idx", + "columns": [ + { + "expression": "connector_instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "documents_connector_deleted_idx": { + "name": "documents_connector_deleted_idx", + "columns": [ + { + "expression": "connector_instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "documents_connector_instance_id_connector_instances_id_fk": { + "name": "documents_connector_instance_id_connector_instances_id_fk", + "tableFrom": "documents", + "columnsFrom": [ + "connector_instance_id" + ], + "tableTo": "connector_instances", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.intelligence_channel_mappings": { + "name": "intelligence_channel_mappings", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_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": { + "intelligence_channel_mappings_thread_idx": { + "name": "intelligence_channel_mappings_thread_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "intelligence_channel_mappings_user_id_users_id_fk": { + "name": "intelligence_channel_mappings_user_id_users_id_fk", + "tableFrom": "intelligence_channel_mappings", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "intelligence_channel_mappings_channel_id_channels_id_fk": { + "name": "intelligence_channel_mappings_channel_id_channels_id_fk", + "tableFrom": "intelligence_channel_mappings", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "intelligence_channel_mappings_user_id_channel_id_pk": { + "name": "intelligence_channel_mappings_user_id_channel_id_pk", + "columns": [ + "user_id", + "channel_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "columns": [ + "token" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_runs": { + "name": "sync_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stats": { + "name": "stats", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sync_runs_connector_started_at_idx": { + "name": "sync_runs_connector_started_at_idx", + "columns": [ + { + "expression": "connector_instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "sync_runs_connector_instance_id_connector_instances_id_fk": { + "name": "sync_runs_connector_instance_id_connector_instances_id_fk", + "tableFrom": "sync_runs", + "columnsFrom": [ + "connector_instance_id" + ], + "tableTo": "connector_instances", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "user_roles_user_id_role_pk": { + "name": "user_roles_user_id_role_pk", + "columns": [ + "user_id", + "role" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "groups": { + "name": "groups", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "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": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_subscriptions": { + "name": "webhook_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "connector_instance_id": { + "name": "connector_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "webhook_subscriptions_connector_instance_id_connector_instances_id_fk": { + "name": "webhook_subscriptions_connector_instance_id_connector_instances_id_fk", + "tableFrom": "webhook_subscriptions", + "columnsFrom": [ + "connector_instance_id" + ], + "tableTo": "connector_instances", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.action_policy": { + "name": "action_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deny": { + "name": "deny", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "approve": { + "name": "approve", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "allow": { + "name": "allow", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.codex_thread_mappings": { + "name": "codex_thread_mappings", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "intelligence_thread_id": { + "name": "intelligence_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "codex_thread_id": { + "name": "codex_thread_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": {}, + "foreignKeys": { + "codex_thread_mappings_user_id_users_id_fk": { + "name": "codex_thread_mappings_user_id_users_id_fk", + "tableFrom": "codex_thread_mappings", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "codex_thread_mappings_agent_id_agents_id_fk": { + "name": "codex_thread_mappings_agent_id_agents_id_fk", + "tableFrom": "codex_thread_mappings", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "codex_thread_mappings_user_id_agent_id_intelligence_thread_id_pk": { + "name": "codex_thread_mappings_user_id_agent_id_intelligence_thread_id_pk", + "columns": [ + "user_id", + "agent_id", + "intelligence_thread_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.codex_user_preferences": { + "name": "codex_user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "codex_user_preferences_user_id_users_id_fk": { + "name": "codex_user_preferences_user_id_users_id_fk", + "tableFrom": "codex_user_preferences", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_bundles": { + "name": "bot_bundles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "bot_bundles_owner_name_version_key": { + "name": "bot_bundles_owner_name_version_key", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "bot_bundles_owner_status_idx": { + "name": "bot_bundles_owner_status_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "bot_bundles_owner_user_id_users_id_fk": { + "name": "bot_bundles_owner_user_id_users_id_fk", + "tableFrom": "bot_bundles", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.continuity_messages": { + "name": "continuity_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor": { + "name": "anchor", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "continuity_messages_owner_thread_message_key": { + "name": "continuity_messages_owner_thread_message_key", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "continuity_messages_owner_channel_time_idx": { + "name": "continuity_messages_owner_channel_time_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "continuity_messages_owner_user_id_users_id_fk": { + "name": "continuity_messages_owner_user_id_users_id_fk", + "tableFrom": "continuity_messages", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "continuity_messages_channel_id_channels_id_fk": { + "name": "continuity_messages_channel_id_channels_id_fk", + "tableFrom": "continuity_messages", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.continuity_snapshots": { + "name": "continuity_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "through_message_id": { + "name": "through_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchors": { + "name": "anchors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"items\":[]}'::jsonb" + }, + "token_estimate": { + "name": "token_estimate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_message_count": { + "name": "source_message_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()" + } + }, + "indexes": { + "continuity_snapshots_owner_channel_created_idx": { + "name": "continuity_snapshots_owner_channel_created_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "continuity_snapshots_owner_user_id_users_id_fk": { + "name": "continuity_snapshots_owner_user_id_users_id_fk", + "tableFrom": "continuity_snapshots", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "continuity_snapshots_channel_id_channels_id_fk": { + "name": "continuity_snapshots_channel_id_channels_id_fk", + "tableFrom": "continuity_snapshots", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_usage_events": { + "name": "skill_usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_run_id": { + "name": "task_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'invoked'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_usage_owner_used_idx": { + "name": "skill_usage_owner_used_idx", + "columns": [ + { + "expression": "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 + }, + "skill_usage_skill_used_idx": { + "name": "skill_usage_skill_used_idx", + "columns": [ + { + "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 + } + }, + "foreignKeys": { + "skill_usage_events_owner_user_id_users_id_fk": { + "name": "skill_usage_events_owner_user_id_users_id_fk", + "tableFrom": "skill_usage_events", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "skill_usage_events_skill_id_skills_id_fk": { + "name": "skill_usage_events_skill_id_skills_id_fk", + "tableFrom": "skill_usage_events", + "columnsFrom": [ + "skill_id" + ], + "tableTo": "skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "skill_usage_events_channel_id_channels_id_fk": { + "name": "skill_usage_events_channel_id_channels_id_fk", + "tableFrom": "skill_usage_events", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "skill_usage_events_task_run_id_task_runs_id_fk": { + "name": "skill_usage_events_task_run_id_task_runs_id_fk", + "tableFrom": "skill_usage_events", + "columnsFrom": [ + "task_run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_versions": { + "name": "skill_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance": { + "name": "provenance", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "scan": { + "name": "scan", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "skill_versions_skill_version_key": { + "name": "skill_versions_skill_version_key", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "skill_versions_hash_idx": { + "name": "skill_versions_hash_idx", + "columns": [ + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "skill_versions_skill_id_skills_id_fk": { + "name": "skill_versions_skill_id_skills_id_fk", + "tableFrom": "skill_versions", + "columnsFrom": [ + "skill_id" + ], + "tableTo": "skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "skill_versions_created_by_user_id_users_id_fk": { + "name": "skill_versions_created_by_user_id_users_id_fk", + "tableFrom": "skill_versions", + "columnsFrom": [ + "created_by_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_program_runs": { + "name": "tool_program_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "program_id": { + "name": "program_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_run_id": { + "name": "task_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_names": { + "name": "step_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"items\":[]}'::jsonb" + }, + "trace": { + "name": "trace", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"items\":[]}'::jsonb" + }, + "output": { + "name": "output", + "type": "text", + "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()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tool_program_runs_owner_created_idx": { + "name": "tool_program_runs_owner_created_idx", + "columns": [ + { + "expression": "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 + } + }, + "foreignKeys": { + "tool_program_runs_program_id_tool_programs_id_fk": { + "name": "tool_program_runs_program_id_tool_programs_id_fk", + "tableFrom": "tool_program_runs", + "columnsFrom": [ + "program_id" + ], + "tableTo": "tool_programs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "tool_program_runs_owner_user_id_users_id_fk": { + "name": "tool_program_runs_owner_user_id_users_id_fk", + "tableFrom": "tool_program_runs", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_program_runs_task_run_id_task_runs_id_fk": { + "name": "tool_program_runs_task_run_id_task_runs_id_fk", + "tableFrom": "tool_program_runs", + "columnsFrom": [ + "task_run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_programs": { + "name": "tool_programs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "steps": { + "name": "steps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"items\":[]}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "reviewed_by_user_id": { + "name": "reviewed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "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_programs_owner_agent_name_key": { + "name": "tool_programs_owner_agent_name_key", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "tool_programs_owner_status_idx": { + "name": "tool_programs_owner_status_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_programs_owner_user_id_users_id_fk": { + "name": "tool_programs_owner_user_id_users_id_fk", + "tableFrom": "tool_programs", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_programs_reviewed_by_user_id_users_id_fk": { + "name": "tool_programs_reviewed_by_user_id_users_id_fk", + "tableFrom": "tool_programs", + "columnsFrom": [ + "reviewed_by_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_channel_connections": { + "name": "external_channel_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inbound_secret_hash": { + "name": "inbound_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_inbound_at": { + "name": "last_inbound_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_outbound_at": { + "name": "last_outbound_at", + "type": "timestamp with time zone", + "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": { + "external_connections_owner_status_idx": { + "name": "external_connections_owner_status_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "external_connections_channel_idx": { + "name": "external_connections_channel_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "external_channel_connections_owner_user_id_users_id_fk": { + "name": "external_channel_connections_owner_user_id_users_id_fk", + "tableFrom": "external_channel_connections", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "external_channel_connections_agent_id_agents_id_fk": { + "name": "external_channel_connections_agent_id_agents_id_fk", + "tableFrom": "external_channel_connections", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "external_channel_connections_channel_id_channels_id_fk": { + "name": "external_channel_connections_channel_id_channels_id_fk", + "tableFrom": "external_channel_connections", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "external_channel_connections_credential_id_credentials_id_fk": { + "name": "external_channel_connections_credential_id_credentials_id_fk", + "tableFrom": "external_channel_connections", + "columnsFrom": [ + "credential_id" + ], + "tableTo": "credentials", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_identity_links": { + "name": "external_identity_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_user_id": { + "name": "external_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_conversation_id": { + "name": "external_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time 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_identity_connection_user_idx": { + "name": "external_identity_connection_user_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "external_identity_openbot_user_idx": { + "name": "external_identity_openbot_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "external_identity_links_connection_id_external_channel_connections_id_fk": { + "name": "external_identity_links_connection_id_external_channel_connections_id_fk", + "tableFrom": "external_identity_links", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "external_channel_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "external_identity_links_user_id_users_id_fk": { + "name": "external_identity_links_user_id_users_id_fk", + "tableFrom": "external_identity_links", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_messages": { + "name": "external_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_message_id": { + "name": "external_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_user_id": { + "name": "external_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_id": { + "name": "external_conversation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_run_id": { + "name": "task_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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": { + "external_message_provider_id_idx": { + "name": "external_message_provider_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "external_message_task_idx": { + "name": "external_message_task_idx", + "columns": [ + { + "expression": "task_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "external_message_outbox_idx": { + "name": "external_message_outbox_idx", + "columns": [ + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "external_messages_connection_id_external_channel_connections_id_fk": { + "name": "external_messages_connection_id_external_channel_connections_id_fk", + "tableFrom": "external_messages", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "external_channel_connections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "external_messages_task_run_id_task_runs_id_fk": { + "name": "external_messages_task_run_id_task_runs_id_fk", + "tableFrom": "external_messages", + "columnsFrom": [ + "task_run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_suggestions": { + "name": "memory_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 70 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "promoted_memory_id": { + "name": "promoted_memory_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": { + "memory_suggestions_owner_status_idx": { + "name": "memory_suggestions_owner_status_idx", + "columns": [ + { + "expression": "owner_user_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 + } + }, + "foreignKeys": { + "memory_suggestions_owner_user_id_users_id_fk": { + "name": "memory_suggestions_owner_user_id_users_id_fk", + "tableFrom": "memory_suggestions", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "memory_suggestions_source_run_id_task_runs_id_fk": { + "name": "memory_suggestions_source_run_id_task_runs_id_fk", + "tableFrom": "memory_suggestions", + "columnsFrom": [ + "source_run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "memory_suggestions_agent_id_agents_id_fk": { + "name": "memory_suggestions_agent_id_agents_id_fk", + "tableFrom": "memory_suggestions", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "memory_suggestions_promoted_memory_id_memory_entries_id_fk": { + "name": "memory_suggestions_promoted_memory_id_memory_entries_id_fk", + "tableFrom": "memory_suggestions", + "columnsFrom": [ + "promoted_memory_id" + ], + "tableTo": "memory_entries", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_route_attempts": { + "name": "model_route_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "route_id": { + "name": "route_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "task_run_id": { + "name": "task_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "model_route_attempt_run_ordinal_idx": { + "name": "model_route_attempt_run_ordinal_idx", + "columns": [ + { + "expression": "task_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "model_route_attempts_route_id_model_routes_id_fk": { + "name": "model_route_attempts_route_id_model_routes_id_fk", + "tableFrom": "model_route_attempts", + "columnsFrom": [ + "route_id" + ], + "tableTo": "model_routes", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "model_route_attempts_task_run_id_task_runs_id_fk": { + "name": "model_route_attempts_task_run_id_task_runs_id_fk", + "tableFrom": "model_route_attempts", + "columnsFrom": [ + "task_run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_routes": { + "name": "model_routes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Default route'" + }, + "candidates": { + "name": "candidates", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"items\":[]}'::jsonb" + }, + "retryable_codes": { + "name": "retryable_codes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"items\":[]}'::jsonb" + }, + "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": { + "model_routes_owner_agent_idx": { + "name": "model_routes_owner_agent_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "model_routes_owner_user_id_users_id_fk": { + "name": "model_routes_owner_user_id_users_id_fk", + "tableFrom": "model_routes", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "model_routes_agent_id_agents_id_fk": { + "name": "model_routes_agent_id_agents_id_fk", + "tableFrom": "model_routes", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_proposals": { + "name": "skill_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_skill_id": { + "name": "target_skill_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "base_hash": { + "name": "base_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposal_hash": { + "name": "proposal_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "scan": { + "name": "scan", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "applied_skill_id": { + "name": "applied_skill_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_user_id": { + "name": "reviewed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "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": { + "skill_proposals_owner_status_idx": { + "name": "skill_proposals_owner_status_idx", + "columns": [ + { + "expression": "owner_user_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 + }, + "skill_proposals_source_run_idx": { + "name": "skill_proposals_source_run_idx", + "columns": [ + { + "expression": "source_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "skill_proposals_owner_user_id_users_id_fk": { + "name": "skill_proposals_owner_user_id_users_id_fk", + "tableFrom": "skill_proposals", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "skill_proposals_source_run_id_task_runs_id_fk": { + "name": "skill_proposals_source_run_id_task_runs_id_fk", + "tableFrom": "skill_proposals", + "columnsFrom": [ + "source_run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "skill_proposals_target_skill_id_skills_id_fk": { + "name": "skill_proposals_target_skill_id_skills_id_fk", + "tableFrom": "skill_proposals", + "columnsFrom": [ + "target_skill_id" + ], + "tableTo": "skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "skill_proposals_applied_skill_id_skills_id_fk": { + "name": "skill_proposals_applied_skill_id_skills_id_fk", + "tableFrom": "skill_proposals", + "columnsFrom": [ + "applied_skill_id" + ], + "tableTo": "skills", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "skill_proposals_reviewed_by_user_id_users_id_fk": { + "name": "skill_proposals_reviewed_by_user_id_users_id_fk", + "tableFrom": "skill_proposals", + "columnsFrom": [ + "reviewed_by_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_result_artifacts": { + "name": "tool_result_artifacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_run_id": { + "name": "task_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preview": { + "name": "preview", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tool_result_artifacts_owner_created_idx": { + "name": "tool_result_artifacts_owner_created_idx", + "columns": [ + { + "expression": "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 + }, + "tool_result_artifacts_run_idx": { + "name": "tool_result_artifacts_run_idx", + "columns": [ + { + "expression": "task_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "tool_result_artifacts_owner_user_id_users_id_fk": { + "name": "tool_result_artifacts_owner_user_id_users_id_fk", + "tableFrom": "tool_result_artifacts", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_result_artifacts_task_run_id_task_runs_id_fk": { + "name": "tool_result_artifacts_task_run_id_task_runs_id_fk", + "tableFrom": "tool_result_artifacts", + "columnsFrom": [ + "task_run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "tool_result_artifacts_agent_id_agents_id_fk": { + "name": "tool_result_artifacts_agent_id_agents_id_fk", + "tableFrom": "tool_result_artifacts", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_preferences": { + "name": "agent_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "agent_preferences_user_id_users_id_fk": { + "name": "agent_preferences_user_id_users_id_fk", + "tableFrom": "agent_preferences", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "agent_preferences_agent_id_agents_id_fk": { + "name": "agent_preferences_agent_id_agents_id_fk", + "tableFrom": "agent_preferences", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "agent_preferences_user_id_agent_id_pk": { + "name": "agent_preferences_user_id_agent_id_pk", + "columns": [ + "user_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_profiles": { + "name": "agent_profiles", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_description": { + "name": "role_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_seed": { + "name": "avatar_seed", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "agent_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "callback_token_hash": { + "name": "callback_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_token_issued_at": { + "name": "callback_token_issued_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_at": { + "name": "created_at", + "type": "timestamp with time 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_profiles_visibility_deleted_idx": { + "name": "agent_profiles_visibility_deleted_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "agent_profiles_callback_token_hash_idx": { + "name": "agent_profiles_callback_token_hash_idx", + "columns": [ + { + "expression": "callback_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "agent_profiles_agent_id_agents_id_fk": { + "name": "agent_profiles_agent_id_agents_id_fk", + "tableFrom": "agent_profiles", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "agent_profiles_owner_user_id_users_id_fk": { + "name": "agent_profiles_owner_user_id_users_id_fk", + "tableFrom": "agent_profiles", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_reactions": { + "name": "message_reactions", + "schema": "", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_reactions_channel_message_idx": { + "name": "message_reactions_channel_message_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "message_reactions_channel_id_channels_id_fk": { + "name": "message_reactions_channel_id_channels_id_fk", + "tableFrom": "message_reactions", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "message_reactions_user_id_users_id_fk": { + "name": "message_reactions_user_id_users_id_fk", + "tableFrom": "message_reactions", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "message_reactions_channel_id_message_id_emoji_user_id_pk": { + "name": "message_reactions_channel_id_message_id_emoji_user_id_pk", + "columns": [ + "channel_id", + "message_id", + "emoji", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.component_exclusions": { + "name": "component_exclusions", + "schema": "", + "columns": { + "component_name": { + "name": "component_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "withheld_by": { + "name": "withheld_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "component_exclusions_component_name_components_name_fk": { + "name": "component_exclusions_component_name_components_name_fk", + "tableFrom": "component_exclusions", + "columnsFrom": [ + "component_name" + ], + "tableTo": "components", + "columnsTo": [ + "name" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "component_exclusions_agent_id_agents_id_fk": { + "name": "component_exclusions_agent_id_agents_id_fk", + "tableFrom": "component_exclusions", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "component_exclusions_component_name_agent_id_pk": { + "name": "component_exclusions_component_name_agent_id_pk", + "columns": [ + "component_name", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.component_functions": { + "name": "component_functions", + "schema": "", + "columns": { + "component_name": { + "name": "component_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "function_name": { + "name": "function_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "component_functions_component_name_components_name_fk": { + "name": "component_functions_component_name_components_name_fk", + "tableFrom": "component_functions", + "columnsFrom": [ + "component_name" + ], + "tableTo": "components", + "columnsTo": [ + "name" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "component_functions_component_name_function_name_pk": { + "name": "component_functions_component_name_function_name_pk", + "columns": [ + "component_name", + "function_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.components": { + "name": "components", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_description": { + "name": "draft_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "published_description": { + "name": "published_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendor": { + "name": "vendor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance": { + "name": "provenance", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'first-party'" + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_mode": { + "name": "auth_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'token'" + }, + "tools_refreshed_at": { + "name": "tools_refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_tools": { + "name": "mcp_tools", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "input_schema": { + "name": "input_schema", + "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": {}, + "foreignKeys": { + "mcp_tools_server_id_mcp_servers_id_fk": { + "name": "mcp_tools_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_tools", + "columnsFrom": [ + "server_id" + ], + "tableTo": "mcp_servers", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "mcp_tools_server_id_name_pk": { + "name": "mcp_tools_server_id_name_pk", + "columns": [ + "server_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_grants": { + "name": "plugin_grants", + "schema": "", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "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_grants_agent_idx": { + "name": "plugin_grants_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plugin_grants_agent_id_agents_id_fk": { + "name": "plugin_grants_agent_id_agents_id_fk", + "tableFrom": "plugin_grants", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "plugin_grants_kind_ref_agent_id_pk": { + "name": "plugin_grants_kind_ref_agent_id_pk", + "columns": [ + "kind", + "ref", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandboxed_components": { + "name": "sandboxed_components", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_description": { + "name": "draft_description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_html": { + "name": "draft_html", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_css": { + "name": "draft_css", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_js_functions": { + "name": "draft_js_functions", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "draft_argument_schema": { + "name": "draft_argument_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "published_description": { + "name": "published_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_html": { + "name": "published_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_css": { + "name": "published_css", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_js_functions": { + "name": "published_js_functions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_argument_schema": { + "name": "published_argument_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sample_arguments": { + "name": "sample_arguments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "authored_by": { + "name": "authored_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'yours'" + }, + "lifecycle_status": { + "name": "lifecycle_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pinned_version": { + "name": "pinned_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "installed_by": { + "name": "installed_by", + "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": { + "skills_slug_key": { + "name": "skills_slug_key", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "skills_owner_idx": { + "name": "skills_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "skills_owner_user_id_users_id_fk": { + "name": "skills_owner_user_id_users_id_fk", + "tableFrom": "skills", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_requests": { + "name": "approval_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_for_user_id": { + "name": "requested_for_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bot_id": { + "name": "bot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_rule": { + "name": "policy_rule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_requests_user_status_idx": { + "name": "approval_requests_user_status_idx", + "columns": [ + { + "expression": "requested_for_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "approval_requests_run_status_idx": { + "name": "approval_requests_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "approval_requests_fingerprint_idx": { + "name": "approval_requests_fingerprint_idx", + "columns": [ + { + "expression": "requested_for_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "approval_requests_run_id_task_runs_id_fk": { + "name": "approval_requests_run_id_task_runs_id_fk", + "tableFrom": "approval_requests", + "columnsFrom": [ + "run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "approval_requests_channel_id_channels_id_fk": { + "name": "approval_requests_channel_id_channels_id_fk", + "tableFrom": "approval_requests", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_run_events": { + "name": "task_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_run_events_run_sequence_idx": { + "name": "task_run_events_run_sequence_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "task_run_events_run_id_task_runs_id_fk": { + "name": "task_run_events_run_id_task_runs_id_fk", + "tableFrom": "task_run_events", + "columnsFrom": [ + "run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_runs": { + "name": "task_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "task_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "max_runtime_ms": { + "name": "max_runtime_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 900000 + }, + "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 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "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 + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "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": { + "task_runs_channel_created_idx": { + "name": "task_runs_channel_created_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "task_runs_actor_status_idx": { + "name": "task_runs_actor_status_idx", + "columns": [ + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "task_runs_queue_idx": { + "name": "task_runs_queue_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "task_runs_lease_idx": { + "name": "task_runs_lease_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "task_runs_channel_id_channels_id_fk": { + "name": "task_runs_channel_id_channels_id_fk", + "tableFrom": "task_runs", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "task_runs_agent_id_agents_id_fk": { + "name": "task_runs_agent_id_agents_id_fk", + "tableFrom": "task_runs", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.delegation_messages": { + "name": "delegation_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "delegation_id": { + "name": "delegation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sender_user_id": { + "name": "sender_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_agent_id": { + "name": "sender_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'note'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "delegation_messages_delegation_created_idx": { + "name": "delegation_messages_delegation_created_idx", + "columns": [ + { + "expression": "delegation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "delegation_messages_delegation_id_delegations_id_fk": { + "name": "delegation_messages_delegation_id_delegations_id_fk", + "tableFrom": "delegation_messages", + "columnsFrom": [ + "delegation_id" + ], + "tableTo": "delegations", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "delegation_messages_sender_user_id_users_id_fk": { + "name": "delegation_messages_sender_user_id_users_id_fk", + "tableFrom": "delegation_messages", + "columnsFrom": [ + "sender_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "delegation_messages_sender_agent_id_agents_id_fk": { + "name": "delegation_messages_sender_agent_id_agents_id_fk", + "tableFrom": "delegation_messages", + "columnsFrom": [ + "sender_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.delegations": { + "name": "delegations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_agent_id": { + "name": "source_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_agent_id": { + "name": "target_agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_channel_id": { + "name": "source_channel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_channel_id": { + "name": "target_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_run_id": { + "name": "task_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_delegation_id": { + "name": "parent_delegation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_depth": { + "name": "max_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "max_children": { + "name": "max_children", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 4 + }, + "max_parallel": { + "name": "max_parallel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "budget_minutes": { + "name": "budget_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "steering_status": { + "name": "steering_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "review_required": { + "name": "review_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "expected_output": { + "name": "expected_output", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "delegation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_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": { + "delegations_actor_status_idx": { + "name": "delegations_actor_status_idx", + "columns": [ + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "delegations_target_status_idx": { + "name": "delegations_target_status_idx", + "columns": [ + { + "expression": "target_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "delegations_parent_idx": { + "name": "delegations_parent_idx", + "columns": [ + { + "expression": "parent_delegation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "delegations_actor_user_id_users_id_fk": { + "name": "delegations_actor_user_id_users_id_fk", + "tableFrom": "delegations", + "columnsFrom": [ + "actor_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "delegations_source_agent_id_agents_id_fk": { + "name": "delegations_source_agent_id_agents_id_fk", + "tableFrom": "delegations", + "columnsFrom": [ + "source_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "delegations_target_agent_id_agents_id_fk": { + "name": "delegations_target_agent_id_agents_id_fk", + "tableFrom": "delegations", + "columnsFrom": [ + "target_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "delegations_source_channel_id_channels_id_fk": { + "name": "delegations_source_channel_id_channels_id_fk", + "tableFrom": "delegations", + "columnsFrom": [ + "source_channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "delegations_target_channel_id_channels_id_fk": { + "name": "delegations_target_channel_id_channels_id_fk", + "tableFrom": "delegations", + "columnsFrom": [ + "target_channel_id" + ], + "tableTo": "channels", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "delegations_task_run_id_task_runs_id_fk": { + "name": "delegations_task_run_id_task_runs_id_fk", + "tableFrom": "delegations", + "columnsFrom": [ + "task_run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "delegations_project_id_projects_id_fk": { + "name": "delegations_project_id_projects_id_fk", + "tableFrom": "delegations", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_entries": { + "name": "memory_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "memory_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "memory_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "pinned": { + "name": "pinned", + "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": { + "memory_entries_owner_scope_updated_idx": { + "name": "memory_entries_owner_scope_updated_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "memory_entries_agent_idx": { + "name": "memory_entries_agent_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "memory_entries_project_idx": { + "name": "memory_entries_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "memory_entries_owner_user_id_users_id_fk": { + "name": "memory_entries_owner_user_id_users_id_fk", + "tableFrom": "memory_entries", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "memory_entries_agent_id_agents_id_fk": { + "name": "memory_entries_agent_id_agents_id_fk", + "tableFrom": "memory_entries", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "memory_entries_project_id_projects_id_fk": { + "name": "memory_entries_project_id_projects_id_fk", + "tableFrom": "memory_entries", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "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 + }, + "kind": { + "name": "kind", + "type": "notification_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_url": { + "name": "target_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "read_at": { + "name": "read_at", + "type": "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": { + "notifications_user_read_created_idx": { + "name": "notifications_user_read_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "notifications_user_id_users_id_fk": { + "name": "notifications_user_id_users_id_fk", + "tableFrom": "notifications", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_agents": { + "name": "project_agents", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_agents_project_id_projects_id_fk": { + "name": "project_agents_project_id_projects_id_fk", + "tableFrom": "project_agents", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_agents_agent_id_agents_id_fk": { + "name": "project_agents_agent_id_agents_id_fk", + "tableFrom": "project_agents", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_agents_added_by_user_id_users_id_fk": { + "name": "project_agents_added_by_user_id_users_id_fk", + "tableFrom": "project_agents", + "columnsFrom": [ + "added_by_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": { + "project_agents_project_id_agent_id_pk": { + "name": "project_agents_project_id_agent_id_pk", + "columns": [ + "project_id", + "agent_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_artifacts": { + "name": "project_artifacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_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": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'note'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "project_artifacts_project_updated_idx": { + "name": "project_artifacts_project_updated_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "project_artifacts_project_id_projects_id_fk": { + "name": "project_artifacts_project_id_projects_id_fk", + "tableFrom": "project_artifacts", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_artifacts_created_by_user_id_users_id_fk": { + "name": "project_artifacts_created_by_user_id_users_id_fk", + "tableFrom": "project_artifacts", + "columnsFrom": [ + "created_by_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "project_artifacts_created_by_agent_id_agents_id_fk": { + "name": "project_artifacts_created_by_agent_id_agents_id_fk", + "tableFrom": "project_artifacts", + "columnsFrom": [ + "created_by_agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_members": { + "name": "project_members", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "project_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_members_project_id_projects_id_fk": { + "name": "project_members_project_id_projects_id_fk", + "tableFrom": "project_members", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_members_user_id_users_id_fk": { + "name": "project_members_user_id_users_id_fk", + "tableFrom": "project_members", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "project_members_project_id_user_id_pk": { + "name": "project_members_project_id_user_id_pk", + "columns": [ + "project_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "project_status", + "typeSchema": "public", + "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": { + "projects_owner_status_idx": { + "name": "projects_owner_status_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "projects_owner_user_id_users_id_fk": { + "name": "projects_owner_user_id_users_id_fk", + "tableFrom": "projects", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_dispatches": { + "name": "routine_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_run_id": { + "name": "task_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_dispatches_routine_scheduled_idx": { + "name": "routine_dispatches_routine_scheduled_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "routine_dispatches_task_run_idx": { + "name": "routine_dispatches_task_run_idx", + "columns": [ + { + "expression": "task_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "routine_dispatches_routine_id_routines_id_fk": { + "name": "routine_dispatches_routine_id_routines_id_fk", + "tableFrom": "routine_dispatches", + "columnsFrom": [ + "routine_id" + ], + "tableTo": "routines", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routine_dispatches_task_run_id_task_runs_id_fk": { + "name": "routine_dispatches_task_run_id_task_runs_id_fk", + "tableFrom": "routine_dispatches", + "columnsFrom": [ + "task_run_id" + ], + "tableTo": "task_runs", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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()" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "instruction": { + "name": "instruction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'routine'" + }, + "quiet_token": { + "name": "quiet_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'NO_ACTION'" + }, + "delivery": { + "name": "delivery", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "safeguards": { + "name": "safeguards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "notepad": { + "name": "notepad", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "blueprint_id": { + "name": "blueprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preflight_status": { + "name": "preflight_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "preflight_message": { + "name": "preflight_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "routine_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "trigger": { + "name": "trigger", + "type": "routine_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "schedule": { + "name": "schedule", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "webhook_token_hash": { + "name": "webhook_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_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": { + "routines_owner_status_idx": { + "name": "routines_owner_status_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "routines_due_idx": { + "name": "routines_due_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "routines_owner_user_id_users_id_fk": { + "name": "routines_owner_user_id_users_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routines_agent_id_agents_id_fk": { + "name": "routines_agent_id_agents_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "agent_id" + ], + "tableTo": "agents", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "routines_channel_id_channels_id_fk": { + "name": "routines_channel_id_channels_id_fk", + "tableFrom": "routines", + "columnsFrom": [ + "channel_id" + ], + "tableTo": "channels", + "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": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.acl_effect": { + "name": "acl_effect", + "schema": "public", + "values": [ + "allow", + "deny" + ] + }, + "public.agent_type": { + "name": "agent_type", + "schema": "public", + "values": [ + "built_in", + "remote_ag_ui" + ] + }, + "public.connector_type": { + "name": "connector_type", + "schema": "public", + "values": [ + "google_drive", + "onedrive" + ] + }, + "public.credential_kind": { + "name": "credential_kind", + "schema": "public", + "values": [ + "model", + "connector", + "agent", + "mcp" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "admin", + "user" + ] + }, + "public.sync_status": { + "name": "sync_status", + "schema": "public", + "values": [ + "pending", + "running", + "succeeded", + "failed" + ] + }, + "public.agent_visibility": { + "name": "agent_visibility", + "schema": "public", + "values": [ + "public", + "private" + ] + }, + "public.approval_status": { + "name": "approval_status", + "schema": "public", + "values": [ + "pending", + "approved", + "declined", + "expired", + "cancelled" + ] + }, + "public.task_run_status": { + "name": "task_run_status", + "schema": "public", + "values": [ + "queued", + "running", + "waiting_for_approval", + "waiting_for_input", + "succeeded", + "failed", + "cancelled" + ] + }, + "public.delegation_status": { + "name": "delegation_status", + "schema": "public", + "values": [ + "queued", + "accepted", + "in_progress", + "completed", + "failed", + "cancelled" + ] + }, + "public.memory_kind": { + "name": "memory_kind", + "schema": "public", + "values": [ + "preference", + "fact", + "instruction", + "decision" + ] + }, + "public.memory_scope": { + "name": "memory_scope", + "schema": "public", + "values": [ + "user", + "agent", + "project" + ] + }, + "public.notification_kind": { + "name": "notification_kind", + "schema": "public", + "values": [ + "task", + "approval", + "delegation", + "routine", + "system" + ] + }, + "public.project_member_role": { + "name": "project_member_role", + "schema": "public", + "values": [ + "owner", + "editor", + "viewer" + ] + }, + "public.project_status": { + "name": "project_status", + "schema": "public", + "values": [ + "active", + "archived" + ] + }, + "public.routine_status": { + "name": "routine_status", + "schema": "public", + "values": [ + "active", + "paused", + "archived" + ] + }, + "public.routine_trigger": { + "name": "routine_trigger", + "schema": "public", + "values": [ + "manual", + "schedule", + "webhook" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/server/drizzle/meta/_journal.json b/server/drizzle/meta/_journal.json index c5cd475..d6db6ec 100644 --- a/server/drizzle/meta/_journal.json +++ b/server/drizzle/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1787323440593, "tag": "0010_lonely_kid_colt", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1787509928939, + "tag": "0011_audit_no_truncate", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/server/tests/audit-immutability.integration.test.ts b/server/tests/audit-immutability.integration.test.ts new file mode 100644 index 0000000..7e975ed --- /dev/null +++ b/server/tests/audit-immutability.integration.test.ts @@ -0,0 +1,51 @@ +import { afterAll, describe, expect, test } from "bun:test"; +import { sql } from "drizzle-orm"; +import { createDatabase } from "../src/db/client"; +import { TEST_POOL } from "./support/database"; + +const database = createDatabase( + process.env.DATABASE_URL ?? + "postgres://openbot:openbot@localhost:5432/openbot", + TEST_POOL, +); + +afterAll(async () => { + await database.$client.close(); +}); + +async function refusedAsAppendOnly(work: () => Promise) { + try { + await work(); + return false; + } catch (error) { + let current: unknown = error; + for (let depth = 0; depth < 5 && current; depth += 1) { + if ( + String((current as { message?: string }).message ?? "").includes( + "append-only", + ) + ) { + return true; + } + current = (current as { cause?: unknown }).cause; + } + return false; + } +} + +describe("audit event immutability in PostgreSQL", () => { + test("refuses to truncate the whole audit trail", async () => { + /* + * Always roll back. If the trigger ever regresses, the sentinel error makes sure the test that + * discovers the hole does not also destroy the audit trail of the database it was pointed at. + */ + expect( + await refusedAsAppendOnly(() => + database.transaction(async (transaction) => { + await transaction.execute(sql`truncate table audit_events`); + throw new Error("rollback after unexpected truncate"); + }), + ), + ).toBe(true); + }); +}); diff --git a/server/tests/audit.test.ts b/server/tests/audit.test.ts index 3314373..93ceea8 100644 --- a/server/tests/audit.test.ts +++ b/server/tests/audit.test.ts @@ -111,6 +111,17 @@ describe("audit event immutability", () => { expect(migration).toContain("BEFORE UPDATE OR DELETE ON audit_events"); expect(migration).toContain("Audit events are append-only"); }); + + test("installs a statement trigger that rejects truncation", async () => { + const migration = await readFile( + new URL("../drizzle/0011_audit_no_truncate.sql", import.meta.url), + "utf8", + ); + + expect(migration).toContain("BEFORE TRUNCATE ON audit_events"); + expect(migration).toContain("FOR EACH STATEMENT"); + expect(migration).toContain("prevent_audit_event_mutation()"); + }); }); describe("admin audit API", () => { From 9dc07a35979f0ea80992380b7d4c6129d95d7ac0 Mon Sep 17 00:00:00 2001 From: Cheskie Londinsky Date: Sun, 23 Aug 2026 14:36:07 -0400 Subject: [PATCH 13/15] Keep the shipped Bot ports on loopback too --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c0437a6..f2ad262 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -191,7 +191,7 @@ services: context: . dockerfile: agent-bot/Dockerfile ports: - - "${BOT_PORT:-4200}:4200" + - "127.0.0.1:${BOT_PORT:-4200}:4200" environment: OPENAI_API_KEY: ${OPENAI_API_KEY} MANAGED_AGENT_TOKEN: ${MANAGED_AGENT_TOKEN:-} @@ -211,7 +211,7 @@ services: context: . dockerfile: agent-langgraph/Dockerfile ports: - - "${LANGGRAPH_PORT:-4201}:4201" + - "127.0.0.1:${LANGGRAPH_PORT:-4201}:4201" environment: # The selected provider reads its own key. Models requiring the Responses API use # BOT_RESPONSES_API instead of changing the streaming loop here. From ea647880c77496b108493063dd97517cde1c1786 Mon Sep 17 00:00:00 2001 From: Cheskie Londinsky Date: Sun, 23 Aug 2026 14:42:51 -0400 Subject: [PATCH 14/15] Format the upstream hardening additions --- server/drizzle/meta/0011_snapshot.json | 1038 ++++------------- server/drizzle/meta/_journal.json | 2 +- server/tests/bot-access.test.ts | 4 +- server/tests/config.test.ts | 7 +- server/tests/plugin-store.integration.test.ts | 7 +- 5 files changed, 255 insertions(+), 803 deletions(-) diff --git a/server/drizzle/meta/0011_snapshot.json b/server/drizzle/meta/0011_snapshot.json index 2817704..f8f5ff3 100644 --- a/server/drizzle/meta/0011_snapshot.json +++ b/server/drizzle/meta/0011_snapshot.json @@ -116,13 +116,9 @@ "accounts_user_id_users_id_fk": { "name": "accounts_user_id_users_id_fk", "tableFrom": "accounts", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -194,13 +190,9 @@ "agents_package_id_deployment_packages_id_fk": { "name": "agents_package_id_deployment_packages_id_fk", "tableFrom": "agents", - "columnsFrom": [ - "package_id" - ], + "columnsFrom": ["package_id"], "tableTo": "deployment_packages", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -313,26 +305,18 @@ "channel_agents_channel_id_channels_id_fk": { "name": "channel_agents_channel_id_channels_id_fk", "tableFrom": "channel_agents", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "channel_agents_agent_id_agents_id_fk": { "name": "channel_agents_agent_id_agents_id_fk", "tableFrom": "channel_agents", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -340,10 +324,7 @@ "compositePrimaryKeys": { "channel_agents_channel_id_agent_id_pk": { "name": "channel_agents_channel_id_agent_id_pk", - "columns": [ - "channel_id", - "agent_id" - ] + "columns": ["channel_id", "agent_id"] } }, "uniqueConstraints": {}, @@ -380,26 +361,18 @@ "channel_memberships_channel_id_channels_id_fk": { "name": "channel_memberships_channel_id_channels_id_fk", "tableFrom": "channel_memberships", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "channel_memberships_user_id_users_id_fk": { "name": "channel_memberships_user_id_users_id_fk", "tableFrom": "channel_memberships", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -407,10 +380,7 @@ "compositePrimaryKeys": { "channel_memberships_channel_id_user_id_pk": { "name": "channel_memberships_channel_id_user_id_pk", - "columns": [ - "channel_id", - "user_id" - ] + "columns": ["channel_id", "user_id"] } }, "uniqueConstraints": {}, @@ -520,26 +490,18 @@ "channels_package_id_deployment_packages_id_fk": { "name": "channels_package_id_deployment_packages_id_fk", "tableFrom": "channels", - "columnsFrom": [ - "package_id" - ], + "columnsFrom": ["package_id"], "tableTo": "deployment_packages", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "channels_last_message_agent_id_agents_id_fk": { "name": "channels_last_message_agent_id_agents_id_fk", "tableFrom": "channels", - "columnsFrom": [ - "last_message_agent_id" - ], + "columnsFrom": ["last_message_agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -651,13 +613,9 @@ "chunks_document_id_documents_id_fk": { "name": "chunks_document_id_documents_id_fk", "tableFrom": "chunks", - "columnsFrom": [ - "document_id" - ], + "columnsFrom": ["document_id"], "tableTo": "documents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -697,13 +655,9 @@ "connector_cursors_connector_instance_id_connector_instances_id_fk": { "name": "connector_cursors_connector_instance_id_connector_instances_id_fk", "tableFrom": "connector_cursors", - "columnsFrom": [ - "connector_instance_id" - ], + "columnsFrom": ["connector_instance_id"], "tableTo": "connector_instances", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -846,13 +800,9 @@ "connector_instances_credential_id_credentials_id_fk": { "name": "connector_instances_credential_id_credentials_id_fk", "tableFrom": "connector_instances", - "columnsFrom": [ - "credential_id" - ], + "columnsFrom": ["credential_id"], "tableTo": "credentials", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -977,9 +927,7 @@ "uniqueConstraints": { "deployment_packages_tenant_id_unique": { "name": "deployment_packages_tenant_id_unique", - "columns": [ - "tenant_id" - ], + "columns": ["tenant_id"], "nullsNotDistinct": false } }, @@ -1073,13 +1021,9 @@ "document_acls_document_id_documents_id_fk": { "name": "document_acls_document_id_documents_id_fk", "tableFrom": "document_acls", - "columnsFrom": [ - "document_id" - ], + "columnsFrom": ["document_id"], "tableTo": "documents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1206,13 +1150,9 @@ "documents_connector_instance_id_connector_instances_id_fk": { "name": "documents_connector_instance_id_connector_instances_id_fk", "tableFrom": "documents", - "columnsFrom": [ - "connector_instance_id" - ], + "columnsFrom": ["connector_instance_id"], "tableTo": "connector_instances", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1281,26 +1221,18 @@ "intelligence_channel_mappings_user_id_users_id_fk": { "name": "intelligence_channel_mappings_user_id_users_id_fk", "tableFrom": "intelligence_channel_mappings", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "intelligence_channel_mappings_channel_id_channels_id_fk": { "name": "intelligence_channel_mappings_channel_id_channels_id_fk", "tableFrom": "intelligence_channel_mappings", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1308,10 +1240,7 @@ "compositePrimaryKeys": { "intelligence_channel_mappings_user_id_channel_id_pk": { "name": "intelligence_channel_mappings_user_id_channel_id_pk", - "columns": [ - "user_id", - "channel_id" - ] + "columns": ["user_id", "channel_id"] } }, "uniqueConstraints": {}, @@ -1379,13 +1308,9 @@ "sessions_user_id_users_id_fk": { "name": "sessions_user_id_users_id_fk", "tableFrom": "sessions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1394,9 +1319,7 @@ "uniqueConstraints": { "sessions_token_unique": { "name": "sessions_token_unique", - "columns": [ - "token" - ], + "columns": ["token"], "nullsNotDistinct": false } }, @@ -1481,13 +1404,9 @@ "sync_runs_connector_instance_id_connector_instances_id_fk": { "name": "sync_runs_connector_instance_id_connector_instances_id_fk", "tableFrom": "sync_runs", - "columnsFrom": [ - "connector_instance_id" - ], + "columnsFrom": ["connector_instance_id"], "tableTo": "connector_instances", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1528,13 +1447,9 @@ "user_roles_user_id_users_id_fk": { "name": "user_roles_user_id_users_id_fk", "tableFrom": "user_roles", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1542,10 +1457,7 @@ "compositePrimaryKeys": { "user_roles_user_id_role_pk": { "name": "user_roles_user_id_role_pk", - "columns": [ - "user_id", - "role" - ] + "columns": ["user_id", "role"] } }, "uniqueConstraints": {}, @@ -1616,9 +1528,7 @@ "uniqueConstraints": { "users_email_unique": { "name": "users_email_unique", - "columns": [ - "email" - ], + "columns": ["email"], "nullsNotDistinct": false } }, @@ -1719,13 +1629,9 @@ "webhook_subscriptions_connector_instance_id_connector_instances_id_fk": { "name": "webhook_subscriptions_connector_instance_id_connector_instances_id_fk", "tableFrom": "webhook_subscriptions", - "columnsFrom": [ - "connector_instance_id" - ], + "columnsFrom": ["connector_instance_id"], "tableTo": "connector_instances", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1841,26 +1747,18 @@ "codex_thread_mappings_user_id_users_id_fk": { "name": "codex_thread_mappings_user_id_users_id_fk", "tableFrom": "codex_thread_mappings", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "codex_thread_mappings_agent_id_agents_id_fk": { "name": "codex_thread_mappings_agent_id_agents_id_fk", "tableFrom": "codex_thread_mappings", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -1868,11 +1766,7 @@ "compositePrimaryKeys": { "codex_thread_mappings_user_id_agent_id_intelligence_thread_id_pk": { "name": "codex_thread_mappings_user_id_agent_id_intelligence_thread_id_pk", - "columns": [ - "user_id", - "agent_id", - "intelligence_thread_id" - ] + "columns": ["user_id", "agent_id", "intelligence_thread_id"] } }, "uniqueConstraints": {}, @@ -1922,13 +1816,9 @@ "codex_user_preferences_user_id_users_id_fk": { "name": "codex_user_preferences_user_id_users_id_fk", "tableFrom": "codex_user_preferences", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -2065,13 +1955,9 @@ "bot_bundles_owner_user_id_users_id_fk": { "name": "bot_bundles_owner_user_id_users_id_fk", "tableFrom": "bot_bundles", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -2222,26 +2108,18 @@ "continuity_messages_owner_user_id_users_id_fk": { "name": "continuity_messages_owner_user_id_users_id_fk", "tableFrom": "continuity_messages", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "continuity_messages_channel_id_channels_id_fk": { "name": "continuity_messages_channel_id_channels_id_fk", "tableFrom": "continuity_messages", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -2355,26 +2233,18 @@ "continuity_snapshots_owner_user_id_users_id_fk": { "name": "continuity_snapshots_owner_user_id_users_id_fk", "tableFrom": "continuity_snapshots", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "continuity_snapshots_channel_id_channels_id_fk": { "name": "continuity_snapshots_channel_id_channels_id_fk", "tableFrom": "continuity_snapshots", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -2489,52 +2359,36 @@ "skill_usage_events_owner_user_id_users_id_fk": { "name": "skill_usage_events_owner_user_id_users_id_fk", "tableFrom": "skill_usage_events", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "skill_usage_events_skill_id_skills_id_fk": { "name": "skill_usage_events_skill_id_skills_id_fk", "tableFrom": "skill_usage_events", - "columnsFrom": [ - "skill_id" - ], + "columnsFrom": ["skill_id"], "tableTo": "skills", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "skill_usage_events_channel_id_channels_id_fk": { "name": "skill_usage_events_channel_id_channels_id_fk", "tableFrom": "skill_usage_events", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "skill_usage_events_task_run_id_task_runs_id_fk": { "name": "skill_usage_events_task_run_id_task_runs_id_fk", "tableFrom": "skill_usage_events", - "columnsFrom": [ - "task_run_id" - ], + "columnsFrom": ["task_run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -2662,26 +2516,18 @@ "skill_versions_skill_id_skills_id_fk": { "name": "skill_versions_skill_id_skills_id_fk", "tableFrom": "skill_versions", - "columnsFrom": [ - "skill_id" - ], + "columnsFrom": ["skill_id"], "tableTo": "skills", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "skill_versions_created_by_user_id_users_id_fk": { "name": "skill_versions_created_by_user_id_users_id_fk", "tableFrom": "skill_versions", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -2800,39 +2646,27 @@ "tool_program_runs_program_id_tool_programs_id_fk": { "name": "tool_program_runs_program_id_tool_programs_id_fk", "tableFrom": "tool_program_runs", - "columnsFrom": [ - "program_id" - ], + "columnsFrom": ["program_id"], "tableTo": "tool_programs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "tool_program_runs_owner_user_id_users_id_fk": { "name": "tool_program_runs_owner_user_id_users_id_fk", "tableFrom": "tool_program_runs", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "tool_program_runs_task_run_id_task_runs_id_fk": { "name": "tool_program_runs_task_run_id_task_runs_id_fk", "tableFrom": "tool_program_runs", - "columnsFrom": [ - "task_run_id" - ], + "columnsFrom": ["task_run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -2981,26 +2815,18 @@ "tool_programs_owner_user_id_users_id_fk": { "name": "tool_programs_owner_user_id_users_id_fk", "tableFrom": "tool_programs", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "tool_programs_reviewed_by_user_id_users_id_fk": { "name": "tool_programs_reviewed_by_user_id_users_id_fk", "tableFrom": "tool_programs", - "columnsFrom": [ - "reviewed_by_user_id" - ], + "columnsFrom": ["reviewed_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -3153,52 +2979,36 @@ "external_channel_connections_owner_user_id_users_id_fk": { "name": "external_channel_connections_owner_user_id_users_id_fk", "tableFrom": "external_channel_connections", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "external_channel_connections_agent_id_agents_id_fk": { "name": "external_channel_connections_agent_id_agents_id_fk", "tableFrom": "external_channel_connections", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "external_channel_connections_channel_id_channels_id_fk": { "name": "external_channel_connections_channel_id_channels_id_fk", "tableFrom": "external_channel_connections", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "external_channel_connections_credential_id_credentials_id_fk": { "name": "external_channel_connections_credential_id_credentials_id_fk", "tableFrom": "external_channel_connections", - "columnsFrom": [ - "credential_id" - ], + "columnsFrom": ["credential_id"], "tableTo": "credentials", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -3315,26 +3125,18 @@ "external_identity_links_connection_id_external_channel_connections_id_fk": { "name": "external_identity_links_connection_id_external_channel_connections_id_fk", "tableFrom": "external_identity_links", - "columnsFrom": [ - "connection_id" - ], + "columnsFrom": ["connection_id"], "tableTo": "external_channel_connections", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "external_identity_links_user_id_users_id_fk": { "name": "external_identity_links_user_id_users_id_fk", "tableFrom": "external_identity_links", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -3546,26 +3348,18 @@ "external_messages_connection_id_external_channel_connections_id_fk": { "name": "external_messages_connection_id_external_channel_connections_id_fk", "tableFrom": "external_messages", - "columnsFrom": [ - "connection_id" - ], + "columnsFrom": ["connection_id"], "tableTo": "external_channel_connections", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "external_messages_task_run_id_task_runs_id_fk": { "name": "external_messages_task_run_id_task_runs_id_fk", "tableFrom": "external_messages", - "columnsFrom": [ - "task_run_id" - ], + "columnsFrom": ["task_run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -3697,52 +3491,36 @@ "memory_suggestions_owner_user_id_users_id_fk": { "name": "memory_suggestions_owner_user_id_users_id_fk", "tableFrom": "memory_suggestions", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memory_suggestions_source_run_id_task_runs_id_fk": { "name": "memory_suggestions_source_run_id_task_runs_id_fk", "tableFrom": "memory_suggestions", - "columnsFrom": [ - "source_run_id" - ], + "columnsFrom": ["source_run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "memory_suggestions_agent_id_agents_id_fk": { "name": "memory_suggestions_agent_id_agents_id_fk", "tableFrom": "memory_suggestions", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memory_suggestions_promoted_memory_id_memory_entries_id_fk": { "name": "memory_suggestions_promoted_memory_id_memory_entries_id_fk", "tableFrom": "memory_suggestions", - "columnsFrom": [ - "promoted_memory_id" - ], + "columnsFrom": ["promoted_memory_id"], "tableTo": "memory_entries", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -3841,26 +3619,18 @@ "model_route_attempts_route_id_model_routes_id_fk": { "name": "model_route_attempts_route_id_model_routes_id_fk", "tableFrom": "model_route_attempts", - "columnsFrom": [ - "route_id" - ], + "columnsFrom": ["route_id"], "tableTo": "model_routes", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "model_route_attempts_task_run_id_task_runs_id_fk": { "name": "model_route_attempts_task_run_id_task_runs_id_fk", "tableFrom": "model_route_attempts", - "columnsFrom": [ - "task_run_id" - ], + "columnsFrom": ["task_run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -3964,26 +3734,18 @@ "model_routes_owner_user_id_users_id_fk": { "name": "model_routes_owner_user_id_users_id_fk", "tableFrom": "model_routes", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "model_routes_agent_id_agents_id_fk": { "name": "model_routes_agent_id_agents_id_fk", "tableFrom": "model_routes", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4167,65 +3929,45 @@ "skill_proposals_owner_user_id_users_id_fk": { "name": "skill_proposals_owner_user_id_users_id_fk", "tableFrom": "skill_proposals", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "skill_proposals_source_run_id_task_runs_id_fk": { "name": "skill_proposals_source_run_id_task_runs_id_fk", "tableFrom": "skill_proposals", - "columnsFrom": [ - "source_run_id" - ], + "columnsFrom": ["source_run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "skill_proposals_target_skill_id_skills_id_fk": { "name": "skill_proposals_target_skill_id_skills_id_fk", "tableFrom": "skill_proposals", - "columnsFrom": [ - "target_skill_id" - ], + "columnsFrom": ["target_skill_id"], "tableTo": "skills", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "skill_proposals_applied_skill_id_skills_id_fk": { "name": "skill_proposals_applied_skill_id_skills_id_fk", "tableFrom": "skill_proposals", - "columnsFrom": [ - "applied_skill_id" - ], + "columnsFrom": ["applied_skill_id"], "tableTo": "skills", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "skill_proposals_reviewed_by_user_id_users_id_fk": { "name": "skill_proposals_reviewed_by_user_id_users_id_fk", "tableFrom": "skill_proposals", - "columnsFrom": [ - "reviewed_by_user_id" - ], + "columnsFrom": ["reviewed_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -4345,39 +4087,27 @@ "tool_result_artifacts_owner_user_id_users_id_fk": { "name": "tool_result_artifacts_owner_user_id_users_id_fk", "tableFrom": "tool_result_artifacts", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "tool_result_artifacts_task_run_id_task_runs_id_fk": { "name": "tool_result_artifacts_task_run_id_task_runs_id_fk", "tableFrom": "tool_result_artifacts", - "columnsFrom": [ - "task_run_id" - ], + "columnsFrom": ["task_run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "tool_result_artifacts_agent_id_agents_id_fk": { "name": "tool_result_artifacts_agent_id_agents_id_fk", "tableFrom": "tool_result_artifacts", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4416,26 +4146,18 @@ "agent_preferences_user_id_users_id_fk": { "name": "agent_preferences_user_id_users_id_fk", "tableFrom": "agent_preferences", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_preferences_agent_id_agents_id_fk": { "name": "agent_preferences_agent_id_agents_id_fk", "tableFrom": "agent_preferences", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4443,10 +4165,7 @@ "compositePrimaryKeys": { "agent_preferences_user_id_agent_id_pk": { "name": "agent_preferences_user_id_agent_id_pk", - "columns": [ - "user_id", - "agent_id" - ] + "columns": ["user_id", "agent_id"] } }, "uniqueConstraints": {}, @@ -4570,26 +4289,18 @@ "agent_profiles_agent_id_agents_id_fk": { "name": "agent_profiles_agent_id_agents_id_fk", "tableFrom": "agent_profiles", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "agent_profiles_owner_user_id_users_id_fk": { "name": "agent_profiles_owner_user_id_users_id_fk", "tableFrom": "agent_profiles", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -4663,26 +4374,18 @@ "message_reactions_channel_id_channels_id_fk": { "name": "message_reactions_channel_id_channels_id_fk", "tableFrom": "message_reactions", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "message_reactions_user_id_users_id_fk": { "name": "message_reactions_user_id_users_id_fk", "tableFrom": "message_reactions", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4690,12 +4393,7 @@ "compositePrimaryKeys": { "message_reactions_channel_id_message_id_emoji_user_id_pk": { "name": "message_reactions_channel_id_message_id_emoji_user_id_pk", - "columns": [ - "channel_id", - "message_id", - "emoji", - "user_id" - ] + "columns": ["channel_id", "message_id", "emoji", "user_id"] } }, "uniqueConstraints": {}, @@ -4745,26 +4443,18 @@ "component_exclusions_component_name_components_name_fk": { "name": "component_exclusions_component_name_components_name_fk", "tableFrom": "component_exclusions", - "columnsFrom": [ - "component_name" - ], + "columnsFrom": ["component_name"], "tableTo": "components", - "columnsTo": [ - "name" - ], + "columnsTo": ["name"], "onUpdate": "no action", "onDelete": "cascade" }, "component_exclusions_agent_id_agents_id_fk": { "name": "component_exclusions_agent_id_agents_id_fk", "tableFrom": "component_exclusions", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4772,10 +4462,7 @@ "compositePrimaryKeys": { "component_exclusions_component_name_agent_id_pk": { "name": "component_exclusions_component_name_agent_id_pk", - "columns": [ - "component_name", - "agent_id" - ] + "columns": ["component_name", "agent_id"] } }, "uniqueConstraints": {}, @@ -4825,13 +4512,9 @@ "component_functions_component_name_components_name_fk": { "name": "component_functions_component_name_components_name_fk", "tableFrom": "component_functions", - "columnsFrom": [ - "component_name" - ], + "columnsFrom": ["component_name"], "tableTo": "components", - "columnsTo": [ - "name" - ], + "columnsTo": ["name"], "onUpdate": "no action", "onDelete": "cascade" } @@ -4839,10 +4522,7 @@ "compositePrimaryKeys": { "component_functions_component_name_function_name_pk": { "name": "component_functions_component_name_function_name_pk", - "columns": [ - "component_name", - "function_name" - ] + "columns": ["component_name", "function_name"] } }, "uniqueConstraints": {}, @@ -5058,13 +4738,9 @@ "mcp_tools_server_id_mcp_servers_id_fk": { "name": "mcp_tools_server_id_mcp_servers_id_fk", "tableFrom": "mcp_tools", - "columnsFrom": [ - "server_id" - ], + "columnsFrom": ["server_id"], "tableTo": "mcp_servers", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5072,10 +4748,7 @@ "compositePrimaryKeys": { "mcp_tools_server_id_name_pk": { "name": "mcp_tools_server_id_name_pk", - "columns": [ - "server_id", - "name" - ] + "columns": ["server_id", "name"] } }, "uniqueConstraints": {}, @@ -5147,13 +4820,9 @@ "plugin_grants_agent_id_agents_id_fk": { "name": "plugin_grants_agent_id_agents_id_fk", "tableFrom": "plugin_grants", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5161,11 +4830,7 @@ "compositePrimaryKeys": { "plugin_grants_kind_ref_agent_id_pk": { "name": "plugin_grants_kind_ref_agent_id_pk", - "columns": [ - "kind", - "ref", - "agent_id" - ] + "columns": ["kind", "ref", "agent_id"] } }, "uniqueConstraints": {}, @@ -5427,13 +5092,9 @@ "skills_owner_user_id_users_id_fk": { "name": "skills_owner_user_id_users_id_fk", "tableFrom": "skills", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5643,26 +5304,18 @@ "approval_requests_run_id_task_runs_id_fk": { "name": "approval_requests_run_id_task_runs_id_fk", "tableFrom": "approval_requests", - "columnsFrom": [ - "run_id" - ], + "columnsFrom": ["run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "approval_requests_channel_id_channels_id_fk": { "name": "approval_requests_channel_id_channels_id_fk", "tableFrom": "approval_requests", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5743,13 +5396,9 @@ "task_run_events_run_id_task_runs_id_fk": { "name": "task_run_events_run_id_task_runs_id_fk", "tableFrom": "task_run_events", - "columnsFrom": [ - "run_id" - ], + "columnsFrom": ["run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -5989,26 +5638,18 @@ "task_runs_channel_id_channels_id_fk": { "name": "task_runs_channel_id_channels_id_fk", "tableFrom": "task_runs", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "task_runs_agent_id_agents_id_fk": { "name": "task_runs_agent_id_agents_id_fk", "tableFrom": "task_runs", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -6103,39 +5744,27 @@ "delegation_messages_delegation_id_delegations_id_fk": { "name": "delegation_messages_delegation_id_delegations_id_fk", "tableFrom": "delegation_messages", - "columnsFrom": [ - "delegation_id" - ], + "columnsFrom": ["delegation_id"], "tableTo": "delegations", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "delegation_messages_sender_user_id_users_id_fk": { "name": "delegation_messages_sender_user_id_users_id_fk", "tableFrom": "delegation_messages", - "columnsFrom": [ - "sender_user_id" - ], + "columnsFrom": ["sender_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "delegation_messages_sender_agent_id_agents_id_fk": { "name": "delegation_messages_sender_agent_id_agents_id_fk", "tableFrom": "delegation_messages", - "columnsFrom": [ - "sender_agent_id" - ], + "columnsFrom": ["sender_agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -6396,91 +6025,63 @@ "delegations_actor_user_id_users_id_fk": { "name": "delegations_actor_user_id_users_id_fk", "tableFrom": "delegations", - "columnsFrom": [ - "actor_user_id" - ], + "columnsFrom": ["actor_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "delegations_source_agent_id_agents_id_fk": { "name": "delegations_source_agent_id_agents_id_fk", "tableFrom": "delegations", - "columnsFrom": [ - "source_agent_id" - ], + "columnsFrom": ["source_agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "delegations_target_agent_id_agents_id_fk": { "name": "delegations_target_agent_id_agents_id_fk", "tableFrom": "delegations", - "columnsFrom": [ - "target_agent_id" - ], + "columnsFrom": ["target_agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "delegations_source_channel_id_channels_id_fk": { "name": "delegations_source_channel_id_channels_id_fk", "tableFrom": "delegations", - "columnsFrom": [ - "source_channel_id" - ], + "columnsFrom": ["source_channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "delegations_target_channel_id_channels_id_fk": { "name": "delegations_target_channel_id_channels_id_fk", "tableFrom": "delegations", - "columnsFrom": [ - "target_channel_id" - ], + "columnsFrom": ["target_channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "delegations_task_run_id_task_runs_id_fk": { "name": "delegations_task_run_id_task_runs_id_fk", "tableFrom": "delegations", - "columnsFrom": [ - "task_run_id" - ], + "columnsFrom": ["task_run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "delegations_project_id_projects_id_fk": { "name": "delegations_project_id_projects_id_fk", "tableFrom": "delegations", - "columnsFrom": [ - "project_id" - ], + "columnsFrom": ["project_id"], "tableTo": "projects", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -6645,39 +6246,27 @@ "memory_entries_owner_user_id_users_id_fk": { "name": "memory_entries_owner_user_id_users_id_fk", "tableFrom": "memory_entries", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memory_entries_agent_id_agents_id_fk": { "name": "memory_entries_agent_id_agents_id_fk", "tableFrom": "memory_entries", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "memory_entries_project_id_projects_id_fk": { "name": "memory_entries_project_id_projects_id_fk", "tableFrom": "memory_entries", - "columnsFrom": [ - "project_id" - ], + "columnsFrom": ["project_id"], "tableTo": "projects", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -6784,13 +6373,9 @@ "notifications_user_id_users_id_fk": { "name": "notifications_user_id_users_id_fk", "tableFrom": "notifications", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -6836,39 +6421,27 @@ "project_agents_project_id_projects_id_fk": { "name": "project_agents_project_id_projects_id_fk", "tableFrom": "project_agents", - "columnsFrom": [ - "project_id" - ], + "columnsFrom": ["project_id"], "tableTo": "projects", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "project_agents_agent_id_agents_id_fk": { "name": "project_agents_agent_id_agents_id_fk", "tableFrom": "project_agents", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "project_agents_added_by_user_id_users_id_fk": { "name": "project_agents_added_by_user_id_users_id_fk", "tableFrom": "project_agents", - "columnsFrom": [ - "added_by_user_id" - ], + "columnsFrom": ["added_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -6876,10 +6449,7 @@ "compositePrimaryKeys": { "project_agents_project_id_agent_id_pk": { "name": "project_agents_project_id_agent_id_pk", - "columns": [ - "project_id", - "agent_id" - ] + "columns": ["project_id", "agent_id"] } }, "uniqueConstraints": {}, @@ -6991,39 +6561,27 @@ "project_artifacts_project_id_projects_id_fk": { "name": "project_artifacts_project_id_projects_id_fk", "tableFrom": "project_artifacts", - "columnsFrom": [ - "project_id" - ], + "columnsFrom": ["project_id"], "tableTo": "projects", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "project_artifacts_created_by_user_id_users_id_fk": { "name": "project_artifacts_created_by_user_id_users_id_fk", "tableFrom": "project_artifacts", - "columnsFrom": [ - "created_by_user_id" - ], + "columnsFrom": ["created_by_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" }, "project_artifacts_created_by_agent_id_agents_id_fk": { "name": "project_artifacts_created_by_agent_id_agents_id_fk", "tableFrom": "project_artifacts", - "columnsFrom": [ - "created_by_agent_id" - ], + "columnsFrom": ["created_by_agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -7070,26 +6628,18 @@ "project_members_project_id_projects_id_fk": { "name": "project_members_project_id_projects_id_fk", "tableFrom": "project_members", - "columnsFrom": [ - "project_id" - ], + "columnsFrom": ["project_id"], "tableTo": "projects", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "project_members_user_id_users_id_fk": { "name": "project_members_user_id_users_id_fk", "tableFrom": "project_members", - "columnsFrom": [ - "user_id" - ], + "columnsFrom": ["user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7097,10 +6647,7 @@ "compositePrimaryKeys": { "project_members_project_id_user_id_pk": { "name": "project_members_project_id_user_id_pk", - "columns": [ - "project_id", - "user_id" - ] + "columns": ["project_id", "user_id"] } }, "uniqueConstraints": {}, @@ -7188,13 +6735,9 @@ "projects_owner_user_id_users_id_fk": { "name": "projects_owner_user_id_users_id_fk", "tableFrom": "projects", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7290,26 +6833,18 @@ "routine_dispatches_routine_id_routines_id_fk": { "name": "routine_dispatches_routine_id_routines_id_fk", "tableFrom": "routine_dispatches", - "columnsFrom": [ - "routine_id" - ], + "columnsFrom": ["routine_id"], "tableTo": "routines", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "routine_dispatches_task_run_id_task_runs_id_fk": { "name": "routine_dispatches_task_run_id_task_runs_id_fk", "tableFrom": "routine_dispatches", - "columnsFrom": [ - "task_run_id" - ], + "columnsFrom": ["task_run_id"], "tableTo": "task_runs", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" } @@ -7539,52 +7074,36 @@ "routines_owner_user_id_users_id_fk": { "name": "routines_owner_user_id_users_id_fk", "tableFrom": "routines", - "columnsFrom": [ - "owner_user_id" - ], + "columnsFrom": ["owner_user_id"], "tableTo": "users", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "routines_agent_id_agents_id_fk": { "name": "routines_agent_id_agents_id_fk", "tableFrom": "routines", - "columnsFrom": [ - "agent_id" - ], + "columnsFrom": ["agent_id"], "tableTo": "agents", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "cascade" }, "routines_channel_id_channels_id_fk": { "name": "routines_channel_id_channels_id_fk", "tableFrom": "routines", - "columnsFrom": [ - "channel_id" - ], + "columnsFrom": ["channel_id"], "tableTo": "channels", - "columnsTo": [ - "id" - ], + "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" - ], + "columnsFrom": ["project_id"], "tableTo": "projects", - "columnsTo": [ - "id" - ], + "columnsTo": ["id"], "onUpdate": "no action", "onDelete": "set null" } @@ -7600,73 +7119,42 @@ "public.acl_effect": { "name": "acl_effect", "schema": "public", - "values": [ - "allow", - "deny" - ] + "values": ["allow", "deny"] }, "public.agent_type": { "name": "agent_type", "schema": "public", - "values": [ - "built_in", - "remote_ag_ui" - ] + "values": ["built_in", "remote_ag_ui"] }, "public.connector_type": { "name": "connector_type", "schema": "public", - "values": [ - "google_drive", - "onedrive" - ] + "values": ["google_drive", "onedrive"] }, "public.credential_kind": { "name": "credential_kind", "schema": "public", - "values": [ - "model", - "connector", - "agent", - "mcp" - ] + "values": ["model", "connector", "agent", "mcp"] }, "public.role": { "name": "role", "schema": "public", - "values": [ - "admin", - "user" - ] + "values": ["admin", "user"] }, "public.sync_status": { "name": "sync_status", "schema": "public", - "values": [ - "pending", - "running", - "succeeded", - "failed" - ] + "values": ["pending", "running", "succeeded", "failed"] }, "public.agent_visibility": { "name": "agent_visibility", "schema": "public", - "values": [ - "public", - "private" - ] + "values": ["public", "private"] }, "public.approval_status": { "name": "approval_status", "schema": "public", - "values": [ - "pending", - "approved", - "declined", - "expired", - "cancelled" - ] + "values": ["pending", "approved", "declined", "expired", "cancelled"] }, "public.task_run_status": { "name": "task_run_status", @@ -7696,67 +7184,37 @@ "public.memory_kind": { "name": "memory_kind", "schema": "public", - "values": [ - "preference", - "fact", - "instruction", - "decision" - ] + "values": ["preference", "fact", "instruction", "decision"] }, "public.memory_scope": { "name": "memory_scope", "schema": "public", - "values": [ - "user", - "agent", - "project" - ] + "values": ["user", "agent", "project"] }, "public.notification_kind": { "name": "notification_kind", "schema": "public", - "values": [ - "task", - "approval", - "delegation", - "routine", - "system" - ] + "values": ["task", "approval", "delegation", "routine", "system"] }, "public.project_member_role": { "name": "project_member_role", "schema": "public", - "values": [ - "owner", - "editor", - "viewer" - ] + "values": ["owner", "editor", "viewer"] }, "public.project_status": { "name": "project_status", "schema": "public", - "values": [ - "active", - "archived" - ] + "values": ["active", "archived"] }, "public.routine_status": { "name": "routine_status", "schema": "public", - "values": [ - "active", - "paused", - "archived" - ] + "values": ["active", "paused", "archived"] }, "public.routine_trigger": { "name": "routine_trigger", "schema": "public", - "values": [ - "manual", - "schedule", - "webhook" - ] + "values": ["manual", "schedule", "webhook"] } }, "schemas": {}, @@ -7769,4 +7227,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/server/drizzle/meta/_journal.json b/server/drizzle/meta/_journal.json index d6db6ec..0a4f098 100644 --- a/server/drizzle/meta/_journal.json +++ b/server/drizzle/meta/_journal.json @@ -87,4 +87,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/server/tests/bot-access.test.ts b/server/tests/bot-access.test.ts index a72eee6..af40424 100644 --- a/server/tests/bot-access.test.ts +++ b/server/tests/bot-access.test.ts @@ -287,9 +287,7 @@ describe("deployment-wide computer routes", () => { test("still checks a Bot path underneath the reserved name", async () => { const { hono, asked, reached } = app(); - const response = await hono.request( - "http://t/api/computers/policy/status", - ); + const response = await hono.request("http://t/api/computers/policy/status"); expect(response.status).toBe(404); expect(asked).toEqual(["policy"]); diff --git a/server/tests/config.test.ts b/server/tests/config.test.ts index 02dbc35..45de46a 100644 --- a/server/tests/config.test.ts +++ b/server/tests/config.test.ts @@ -236,12 +236,13 @@ describe("deployment configuration", () => { ...baseEnvironment, NODE_ENV: "production", TRUSTED_ORIGINS: "https://openbot.example.com", - KEY_ENCRYPTION_KEY: - "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", + KEY_ENCRYPTION_KEY: "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", AGENT_COMPUTER_URL: "http://computer:4100", AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS: "true", }), - ).toThrow(/AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true is for local development only/); + ).toThrow( + /AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true is for local development only/, + ); }); test("rejects trusted origins that include a path", () => { diff --git a/server/tests/plugin-store.integration.test.ts b/server/tests/plugin-store.integration.test.ts index bc3e536..3ab6543 100644 --- a/server/tests/plugin-store.integration.test.ts +++ b/server/tests/plugin-store.integration.test.ts @@ -194,12 +194,7 @@ describe("a grant is the permission", () => { const offered = await store.listForAgent(holderId); expect(offered.tools.some((tool) => tool.ref === missingRef)).toBe(false); } finally { - await store.revoke( - "mcp", - missingRef, - holderId, - "admin@openbot.local", - ); + await store.revoke("mcp", missingRef, holderId, "admin@openbot.local"); } }); }); From 675a0ccce2853b8ccc430311eb097bb073228de8 Mon Sep 17 00:00:00 2001 From: Cheskie Londinsky Date: Sun, 23 Aug 2026 14:47:30 -0400 Subject: [PATCH 15/15] Keep managed secrets and neutral policy fields in their lanes --- server/src/approvals/store.ts | 9 +++-- server/src/computer/policy.ts | 4 ++- server/src/network/outbound.ts | 2 ++ server/tests/computer-policy.test.ts | 19 ++++++++++ server/tests/outbound-network.test.ts | 35 +++++++++++++++++++ .../tests/runs-approvals.integration.test.ts | 32 +++++++++++++++++ 6 files changed, 97 insertions(+), 4 deletions(-) diff --git a/server/src/approvals/store.ts b/server/src/approvals/store.ts index 4a443bb..ff794c4 100644 --- a/server/src/approvals/store.ts +++ b/server/src/approvals/store.ts @@ -185,15 +185,18 @@ function actionDescription(toolName: string, context: PolicyContext) { const details: Array<{ label: string; value: string }> = [ { label: "Action", value: toolName }, ]; - if (context.element) { + // Browser/file actions bind empty objects for fields they do not use so CEL rules remain + // evaluable. Do not turn those neutral bindings into empty approval details. + if (context.element?.ref) { details.push({ label: "Target", value: context.element.name }); details.push({ label: "Element", value: context.element.role }); } if (context.page.url) details.push({ label: "Page", value: context.page.url }); - if (context.file) details.push({ label: "File", value: context.file.path }); + if (context.file?.path) + details.push({ label: "File", value: context.file.path }); if (context.key) details.push({ label: "Key", value: context.key }); - if (context.mcp) { + if (context.mcp?.effect) { details.push({ label: "Service", value: context.mcp.server }); details.push({ label: "Tool", value: context.mcp.tool }); } diff --git a/server/src/computer/policy.ts b/server/src/computer/policy.ts index effb7a6..13233f9 100644 --- a/server/src/computer/policy.ts +++ b/server/src/computer/policy.ts @@ -302,7 +302,9 @@ export function evaluateActionPolicy( /** A refusal a person can act on: what was refused, and on what. */ function describeRefusal(context: PolicyContext, expression: string): string { - if (context.mcp) { + // Non-MCP actions carry an empty MCP object so CEL rules can read `mcp.effect` without throwing. + // Only a real effect identifies an MCP call for person-facing descriptions. + if (context.mcp?.effect) { return ( `This deployment's policy does not allow that: ${context.mcp.tool} on ` + `${context.mcp.server} is blocked by the rule \`${expression}\`.` diff --git a/server/src/network/outbound.ts b/server/src/network/outbound.ts index eb5d557..2491717 100644 --- a/server/src/network/outbound.ts +++ b/server/src/network/outbound.ts @@ -1,5 +1,6 @@ import { lookup } from "node:dns/promises"; import { isIP } from "node:net"; +import { MANAGED_AGENT_TOKEN_HEADER } from "../../../shared/agent-authorisation"; import { checkNavigationTarget } from "../computer/target"; export type AddressRecord = { address: string; family: number }; @@ -74,6 +75,7 @@ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]); const SENSITIVE_REDIRECT_HEADERS = [ "authorization", "cookie", + MANAGED_AGENT_TOKEN_HEADER, "proxy-authorization", ]; diff --git a/server/tests/computer-policy.test.ts b/server/tests/computer-policy.test.ts index c9df4dd..ac1ea81 100644 --- a/server/tests/computer-policy.test.ts +++ b/server/tests/computer-policy.test.ts @@ -251,6 +251,25 @@ describe("describing a refusal", () => { ); expect(decision.reason).not.toContain("the file"); }); + + test("a neutral MCP binding still describes the browser action", () => { + const decision = evaluateActionPolicy( + { + mode: "enforce", + deny: ['contains(element.name, "submit")'], + allow: ["true"], + }, + context({ + key: "", + file: { path: "", name: "", extension: "" }, + mcp: { server: "", tool: "", effect: "", argumentsHash: "" }, + }), + ); + + expect(decision.reason).toContain("Submit order"); + expect(decision.reason).toContain("example.com"); + expect(decision.reason).not.toContain(" on is blocked"); + }); }); describe("parseActionPolicy", () => { diff --git a/server/tests/outbound-network.test.ts b/server/tests/outbound-network.test.ts index c1eafe7..70beb4e 100644 --- a/server/tests/outbound-network.test.ts +++ b/server/tests/outbound-network.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { MANAGED_AGENT_TOKEN_HEADER } from "../../shared/agent-authorisation"; import { assertOutboundUrl, createOutboundFetch, @@ -100,4 +101,38 @@ describe("outbound network policy", () => { }); expect(authorizations).toEqual(["Bearer secret", null]); }); + + test("does not forward the managed Bot token across origins", async () => { + const presented: Array<{ + managed: string | null; + ordinary: string | null; + }> = []; + const guarded = createOutboundFetch({ + resolver: async () => [{ address: "93.184.216.34", family: 4 }], + fetchImpl: async (_input, init) => { + const headers = new Headers(init?.headers); + presented.push({ + managed: headers.get(MANAGED_AGENT_TOKEN_HEADER), + ordinary: headers.get("x-request-label"), + }); + return presented.length === 1 + ? new Response(null, { + status: 307, + headers: { location: "https://redirected.example.test/ag-ui" }, + }) + : new Response("ok"); + }, + }); + + await guarded("https://managed.example.test/ag-ui", { + headers: { + [MANAGED_AGENT_TOKEN_HEADER]: "deployment-secret", + "x-request-label": "kept", + }, + }); + expect(presented).toEqual([ + { managed: "deployment-secret", ordinary: "kept" }, + { managed: null, ordinary: "kept" }, + ]); + }); }); diff --git a/server/tests/runs-approvals.integration.test.ts b/server/tests/runs-approvals.integration.test.ts index 7859c7c..af6805e 100644 --- a/server/tests/runs-approvals.integration.test.ts +++ b/server/tests/runs-approvals.integration.test.ts @@ -201,4 +201,36 @@ describe("exact, resumable approvals", () => { }), ).rejects.toBeInstanceOf(ApprovalContextError); }); + + test("keeps neutral policy bindings out of a browser approval", async () => { + const pending = await approvals.authorize({ + actor, + botId: agentId, + toolName: "computer_navigate", + policyRule: 'tool.name == "computer_navigate"', + context: { + tool: { name: "computer_navigate" }, + bot: { id: agentId }, + actor: { id: actor.id }, + page: { + url: "https://example.test/expenses", + host: "example.test", + }, + key: "", + element: { ref: "", role: "", name: "", type: "" }, + file: { path: "", name: "", extension: "" }, + mcp: { server: "", tool: "", effect: "", argumentsHash: "" }, + }, + }); + + expect(pending.authorized).toBe(false); + if (pending.authorized) throw new Error("Expected a pending approval."); + expect(pending.request.summary).toBe( + "Open a website: https://example.test/expenses", + ); + expect(pending.request.details).toEqual([ + { label: "Action", value: "computer_navigate" }, + { label: "Page", value: "https://example.test/expenses" }, + ]); + }); });