diff --git a/.env.example b/.env.example index 7b60228..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 @@ -204,6 +205,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..02646cc 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: @@ -156,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 @@ -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` @@ -203,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. | @@ -254,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/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-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); + } + }); +}); 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/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/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..f2ad262 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. @@ -182,9 +191,10 @@ 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:-} # 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:-} @@ -201,11 +211,12 @@ 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. 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/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/docs/configuration.md b/docs/configuration.md index 696fc43..c829e17 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. | @@ -163,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. | @@ -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/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..f8f5ff3 --- /dev/null +++ b/server/drizzle/meta/0011_snapshot.json @@ -0,0 +1,7230 @@ +{ + "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": {} + } +} diff --git a/server/drizzle/meta/_journal.json b/server/drizzle/meta/_journal.json index c5cd475..0a4f098 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 } ] } 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/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/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/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/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/gateway.ts b/server/src/computer/gateway.ts index a499c7b..cf12bf4 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, @@ -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); @@ -304,6 +310,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/src/computer/policy.ts b/server/src/computer/policy.ts index a1048d0..13233f9 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; }; @@ -301,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/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/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..9d8fc07 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[]; /** @@ -340,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"] { @@ -358,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 @@ -517,6 +540,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/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/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/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/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/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/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", () => { diff --git a/server/tests/bot-access.test.ts b/server/tests/bot-access.test.ts index 687bdc9..af40424 100644 --- a/server/tests/bot-access.test.ts +++ b/server/tests/bot-access.test.ts @@ -251,6 +251,50 @@ 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/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-gateway.test.ts b/server/tests/computer-gateway.test.ts index deb4b59..538a7ed 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, @@ -169,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, @@ -482,13 +500,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"); }); }); diff --git a/server/tests/computer-policy.test.ts b/server/tests/computer-policy.test.ts index b59b40f..ac1ea81 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()); @@ -238,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/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..45de46a 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({ @@ -207,6 +220,31 @@ 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({ 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/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/plugin-store.integration.test.ts b/server/tests/plugin-store.integration.test.ts index 3e1c087..3ab6543 100644 --- a/server/tests/plugin-store.integration.test.ts +++ b/server/tests/plugin-store.integration.test.ts @@ -175,6 +175,28 @@ 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", () => { 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" }, + ]); + }); }); 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/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(); + }); +}); 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/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); 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"); + }); +}); 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/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. */ diff --git a/tests/compose.test.ts b/tests/compose.test.ts index 282af72..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. * @@ -62,6 +95,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"),