From dee4d19db031393534bc37ed94e633162926be1f Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 20:53:04 -0500 Subject: [PATCH 001/279] Add isolated external MCP fixture foundation --- docs/adr/0125-isolated-external-mcp.md | 178 +++++++++ docs/testing/external-mcp-baseline.md | 42 ++ mcp-worker/.gitignore | 1 + mcp-worker/README.md | 43 ++ mcp-worker/package-lock.json | 277 +++++++++++++ mcp-worker/package.json | 26 ++ mcp-worker/src/index.ts | 2 + mcp-worker/test/fixture.test.ts | 531 +++++++++++++++++++++++++ mcp-worker/test/fixtures/http.ts | 52 +++ mcp-worker/test/fixtures/oauth.ts | 199 +++++++++ mcp-worker/test/fixtures/server.ts | 451 +++++++++++++++++++++ mcp-worker/test/fixtures/tls.ts | 69 ++++ mcp-worker/tsconfig.build.json | 11 + mcp-worker/tsconfig.json | 16 + 14 files changed, 1898 insertions(+) create mode 100644 docs/adr/0125-isolated-external-mcp.md create mode 100644 docs/testing/external-mcp-baseline.md create mode 100644 mcp-worker/.gitignore create mode 100644 mcp-worker/README.md create mode 100644 mcp-worker/package-lock.json create mode 100644 mcp-worker/package.json create mode 100644 mcp-worker/src/index.ts create mode 100644 mcp-worker/test/fixture.test.ts create mode 100644 mcp-worker/test/fixtures/http.ts create mode 100644 mcp-worker/test/fixtures/oauth.ts create mode 100644 mcp-worker/test/fixtures/server.ts create mode 100644 mcp-worker/test/fixtures/tls.ts create mode 100644 mcp-worker/tsconfig.build.json create mode 100644 mcp-worker/tsconfig.json diff --git a/docs/adr/0125-isolated-external-mcp.md b/docs/adr/0125-isolated-external-mcp.md new file mode 100644 index 000000000..88bbda41d --- /dev/null +++ b/docs/adr/0125-isolated-external-mcp.md @@ -0,0 +1,178 @@ +# ADR 0125 — Isolated external MCP tools + +Status: Proposed (2026-11-01) + +## Context + +ADR 0058 deferred native MCP configuration in the guest. That design lets a +harness connect to an upstream server and does not provide a platform write gate. +External tools must instead use the authenticated ENGRAM_TOOLS rail from ADR 0089. Claude uses its existing bridge; Codex uses its existing dynamic tools. +Both use parked calls and the durable completion outbox. MCP metadata is +untrusted input, never a new process-global tool registry. + +ADR 0106 is the credential authority. MCP OAuth uses subject kind `Mcp` and the +named integration connection ID as subject ID. Organization API keys use the +existing KEK-sealed organization secret store. There is no parallel secret store. +Personal credentials, guest stdio servers, package installation, resources, +prompts, sampling, elicitation, MCP Apps, and asynchronous Tasks are out of scope. + +## Decision + +### Service and trust boundary + +Add a trusted standalone Node 24 ESM service, `mcp-worker`, with the official +`@modelcontextprotocol/client` pinned to 2.4.0, Ajv pinned to 8.20.0, and a +maintained IP parser. Its mTLS interface has fixed discover, execute, cancel, +OAuth metadata, and token-exchange operations. It has no generic proxy or guest +listener. Each operator zone has a separate deployment. It receives no database +credentials, KEK, cloud identity, host mounts, or service-account token. Disable +core dumps and credential disk writes. Credentials exist only in operation memory +and must never appear in logs. + +The orchestrator issues expiring, single-use intents bound to worker boot ID, +connection revision, exact destination, operation, and call digest. Discovery and +OAuth have separate budgets. The worker cannot authorize a tool call or refresh a +credential on its own. The coordinator is the only credential and refresh +authority. The worker receives secrets only after admission, near dispatch. + +### Reviewed catalogs and session binding + +Reuse named integration connections and profile integration grants. Discovery +creates a draft. Only administrators can publish an immutable reviewed catalog, +using expected-revision compare-and-swap. Preserve upstream descriptions, +schemas, and fingerprints. Administrators assign explicit read/write classes; +server annotations confer no permission. + +An immutable task-session binding includes connection/config revision, credential +identity, authorization generation, catalog revision, grants, constraints, and +hashes. Tool names resolve only through that binding. The guest cannot provide +endpoints or credential subjects. Changes to endpoint, account, authentication, +policy, or reviewed tools invalidate the binding. A normal refresh with the same +account and scope preserves it. Rebind must be explicit and cannot widen an +existing session silently. Child grants intersect parent effective grants. Plan +mode permits only reads and comes from trusted state. + +Managed upstream authorities and aliases enter the guest deny set before any +allow or default-allow rule and before credential injection. Reject conflicts +with direct integrations. MCP secret references cannot use generic guest inject +or mint paths. Keep old denies when invalidating; install new denies before a +rebind becomes visible. This guarantee covers organization-managed credentials; +unknown alternate origins and user-supplied credentials are outside it. Upstream +servers must authenticate writes. + +### Network containment + +Accept canonical HTTPS URLs with exact approved authority and port (443 by +default). Reject userinfo, query, fragment, IP literals, and redirects. A public +zone denies private and reserved ranges. An internal zone allows only exact +operator-approved hosts and private CIDRs, with operator-approved CAs. An +administrator cannot widen CIDRs, CAs, or routes. Every zone denies loopback, +link-local, metadata, multicast, unspecified, and platform administration +addresses. Apply this policy to OAuth as well as tools. + +Resolve every A and AAAA record on each new dial. Reject mixed permitted and +forbidden answers. Parse addresses with an established library and pin the checked +IP while preserving TLS SNI and HTTP authority. Local loopback and private test +CA allowances exist only in fixture code, never in production policy. + +### Protocol and validation + +Support only Streamable HTTP with JSON or request-scoped SSE responses, using +modern 2026-07-28 and legacy 2025-11-25 / 2025-06-18. Modern requests have no +initialize handshake or transport session. Legacy sessions are scoped to platform +session and credential identity. Never fall back to the deprecated separate SSE +transport. Untrusted responses cannot trigger resource, link, or icon fetches. + +Preserve text, structuredContent, and isError. Reject binary and resource content. +A delivery failure is distinct from the upstream outcome. Support JSON Schema +Draft 7 and 2020-12, local references only, bounded complexity, and no unsupported +dialects, remote references, or executable formats. Disable coercion, default +insertion, and property deletion. Compile and validate in a disposable child with +a hard one-second deadline and 128 MiB heap. + +### OAuth + +Use pre-registered public or confidential clients, no dynamic client registration. +Require PKCE S256, encrypted durable single-use state with a ten-minute expiry, +exact redirect URI, issuer validation, and resource/audience checks. Independently +approve metadata, authorization, and token destinations. Require explicit admin +consent to share the organization-session account. + +Use single-flight refresh and rotating-token compare-and-swap. Do not fall back +to stale credentials or expand scope silently. Send access tokens only to the +resource; send client secrets and refresh tokens only to the token endpoint. + +### Authorization and irreversible dispatch + +Before execution, validate the active session/run, immutable binding, principal, +ownership, live permissions, current revisions, immutable grants, tool input, and +admin constraints. Writes also need one-use approval or an explicit automation +grant. Owner/admin interactive approval expires in ten minutes and binds the exact +session, run, call, connection, catalog, generation, and canonical argument hash. +It cannot expand grants or be replayed. Automation-plan approval cannot approve +MCP writes. Unrestricted automation constraints `{}` must be selected explicitly. + +Keep the authoritative call journal separate from the UI pending projection. +Use unique `(session_id, tool_call_id)` and immutable request digests. Enforce +`waiting_approval -> authorized -> dispatching -> succeeded/failed/canceled/outcome_unknown` +with explicit legal transitions. Entering `dispatching` is the irreversible write +barrier: never auto-replay a write after crash, lease loss, or timeout. External +effects stay outside retried DBOS steps. Only reviewed safe reads may have bounded +retries. Persist a verified result before the completion outbox obligation. + +An unknown write blocks later writes for the session/connection, even with new +call IDs, until an owner/admin resolves it. Established success with unusable +output stays `succeeded` with result unavailable; otherwise record outcome unknown. +Guest completion projection is not evidence of upstream success. Stop, interrupt, +revoke, and deadline cancel active I/O best effort; cancellation cannot undo an +upstream effect. Resume uses live policy without widening immutable grants and +retains durable late results. + +### Limits and audit + +| Limit | Value | +| ---------------------------------------------- | ----------------------------------- | +| Connections / tools per session | 16 / 128 | +| Manifest / arguments / normalized result | 1 MiB / 256 KiB / 1 MiB | +| Decompressed response | 2 MiB | +| Connection / tool deadline | 5 s / 60 s (operator ceiling 300 s) | +| Concurrency: session / connection / deployment | 4 / 8 / 32 | +| Queue size / expiry | 128 / 30 s | +| Pending approvals: session / deployment | 16 / 128 | +| Schema validation deadline / heap | 1 s / 128 MiB | + +Enforce connection and deployment admission across replicas. Audit metadata only: +identities, revisions, decision, actor, call, timing, sizes, and outcome. Never audit +raw arguments, results, headers, or secrets. Session payloads use existing retention +and access rules. Redact known secret echoes, without claiming protection against +a malicious credential recipient. + +Use additive indexed foreign-key migrations, unique IDs, and state constraints. +Never alter applied migration checksums. Version shared protocols. Coordinator +persistence changes require simulator and real-Postgres conformance tests. + +## Rollout, rollback, and acceptance + +All global, zone, connection, and tool flags default false. Each kill switch blocks +new dispatch and cancels active work while preserving journals. Rollback disables +dispatch and drains work; it never deletes live history or migrations. + +Stage rollout: public reads with API keys, private endpoints, OAuth, interactive +writes, then explicit automation writes. Before enabling any stage, run its hostile +fixture cases, current-head CI, and operator acceptance. Both Claude and Codex must +pass the shared protocol path, approval, denial, cancellation, eviction/resume, +secret containment, and unknown-outcome gates. Require real Firecracker production +acceptance. VZ proves local parity separately and cannot satisfy that gate. Fork +runners are not configured yet; runner provisioning is an external prerequisite. + +The foundation adds only a test fixture and package scaffolding. It does not enable +worker policy or authorize dispatch. The baseline QuickJS deep-recursion failure is +recorded in `docs/testing/external-mcp-baseline.md`; do not weaken or skip it. +This record stays Proposed until the final acceptance review records evidence and +the implementation commit chain. + +## Sources + +- [Streamable HTTP 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) +- [Official TypeScript SDK 2.4.0](https://github.com/modelcontextprotocol/typescript-sdk/releases/tag/v2.4.0) +- [MCP authorization](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) diff --git a/docs/testing/external-mcp-baseline.md b/docs/testing/external-mcp-baseline.md new file mode 100644 index 000000000..d76fd7889 --- /dev/null +++ b/docs/testing/external-mcp-baseline.md @@ -0,0 +1,42 @@ +# External MCP validation baseline + +This records the primary development setup before external MCP changes on +2026-11-01. These are inherited preflight results, not new measurements of the +worker change. They are retained so later checks do not hide an existing failure. +The feature must stay disabled until the release gates pass. + +| Surface | Recorded result | Evidence boundary | +| ------------------------------- | ---------------------------------- | -------------------------------------------------------- | +| Orchestrator without database | 2,427 passed, 219 skipped | Source tests without live Postgres | +| Orchestrator with full Postgres | 2,619 passed, 18 skipped, 1 failed | Full local database suite | +| Web | 1,069 passed; build passed | Local source tests and production build | +| Native backends | Builds passed; VZ demo passed | Local native build and VZ demonstration | +| Firecracker production | Not established | No real production run; fork runners are not provisioned | + +The full Postgres suite failed in `sandbox.test.ts`: the deep-recursion case +expected a QuickJS `SandboxError`, but observed a stack error mismatch. The +focused file then passed all 15 tests. A 100-repeat recursion probe observed +`StackError`. This is an unrelated known baseline issue. Keep its assertion and +suite intact; do not use the focused pass to describe the full suite as green. + +The new `mcp-worker` package uses Node 24. Its deterministic local fixture has +real TLS and the pinned official MCP client. It covers modern and legacy +Streamable HTTP, JSON and SSE, catalog pages and schema drift, read/write +counters, held calls, transport and notification cancellation, hostile result +bodies, and OAuth metadata, PKCE, code replay, and refresh rotation. Its private +CA and loopback transport adapter are test-only. Local fixture success does not +prove isolation, live provider behavior, or production Firecracker acceptance. + +Run the package gates from the repository root: + +```sh +npm ci --prefix mcp-worker --ignore-scripts +npm run format:check --prefix mcp-worker +npm run typecheck --prefix mcp-worker +npm test --prefix mcp-worker +npm run build --prefix mcp-worker +``` + +The rollout/CI task must wire these commands into the path detector and required +`CI Gate` dependency graph before the feature ships. The fixture foundation does +not modify the workflow or enable any production dispatch path. diff --git a/mcp-worker/.gitignore b/mcp-worker/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/mcp-worker/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/mcp-worker/README.md b/mcp-worker/README.md new file mode 100644 index 000000000..acb9d7be4 --- /dev/null +++ b/mcp-worker/README.md @@ -0,0 +1,43 @@ +# MCP worker foundation + +This is a standalone Node 24 TypeScript ESM package. The official client is pinned +to 2.4.0. Ajv 8.20.0 and ipaddr.js are pinned for the worker validation boundary. +`package-lock.json` records exact dependency versions and integrity hashes. + +There is no production service entrypoint or worker policy in this foundation. +The empty source module reserves the package build boundary. Later changes add +fixed authenticated RPCs and network policy. The feature remains disabled. + +```sh +npm ci --ignore-scripts +npm run format:check +npm run typecheck +npm test +npm run build +``` + +`test/fixtures/server.ts` exposes `startMcpFixture(options)`. Each server binds +only loopback on an operating-system assigned port. It exposes `url`, `origin`, +`fetch`, `ca`, request observations, read/write/cancellation counts, +`setCatalogRevision`, `waitForCall`, `waitForCancellation`, `releaseCalls`, and +`close`. Always close fixtures in test cleanup. + +Options select the protocol version, JSON/SSE replies, a held call barrier, +authentication, a callback issuer fault, and normal/malformed/oversize/binary/ +image/audio/resource/resource-link/error results. Held calls release explicitly; +no sleep or wall-clock delay is needed. A write changes the counter before a +held or hostile reply. Cancellation does not roll that effect back. + +The mock authorization server uses fixed public/confidential test clients and +real TLS. It publishes resource and authorization-server metadata, requires +PKCE S256 and exact resource/redirect/client matches, emits callback `iss`, and +rejects code/refresh replay and scope expansion. Confidential-client credentials +are explicit test strings. It is a test provider, not a credential authority for +the product. + +`test/fixtures/tls.ts` contains a public test CA, certificate, and private key, +valid until 2036. `http.ts` trusts that CA explicitly for fixture requests and +never changes global TLS settings. Never import any fixture module into +production. The loopback listener and private CA do not create production +network exceptions. The mock server is handwritten so tests can send invalid +wire data. Tests use the actual official SDK to check the supported happy paths. diff --git a/mcp-worker/package-lock.json b/mcp-worker/package-lock.json new file mode 100644 index 000000000..440196148 --- /dev/null +++ b/mcp-worker/package-lock.json @@ -0,0 +1,277 @@ +{ + "name": "@engrams/mcp-worker", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@engrams/mcp-worker", + "version": "0.0.0", + "dependencies": { + "@modelcontextprotocol/client": "2.4.0", + "ajv": "8.20.0", + "ipaddr.js": "2.2.0" + }, + "devDependencies": { + "@types/node": "24.10.0", + "prettier": "3.6.2", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=24 <25" + } + }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.4.0.tgz", + "integrity": "sha512-LgrD2OphHuG12vfKFKz6SOnq7bKHQmhGJIAYdD+MZBn2npHEyry6uD8zvktodtl5CQM85MO0anUyD9JwmUmxww==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.4.0", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.8", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.4.0.tgz", + "integrity": "sha512-z8QBKqdfXLPzkLQuG4xp8hrVEMm1PZ1IgxMN2sMbJiFiAKOWXnAzogXn2W4aqIBlP1ea6U5yiqVquvUNM1Cj6Q==", + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.15", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.15.tgz", + "integrity": "sha512-qdF6ewcZARtef63fJ9AKKZPgxw+t7USH6HLXYcD4CrdmSvDp79rpJSPFEKc90QBeGS63u1KQtN2MZQHC0yDIzA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/mcp-worker/package.json b/mcp-worker/package.json new file mode 100644 index 000000000..23a13e3bb --- /dev/null +++ b/mcp-worker/package.json @@ -0,0 +1,26 @@ +{ + "name": "@engrams/mcp-worker", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { + "node": ">=24 <25" + }, + "scripts": { + "test": "node --test --test-timeout=10000 test/*.test.ts", + "typecheck": "tsc --noEmit", + "build": "tsc -p tsconfig.build.json", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "dependencies": { + "@modelcontextprotocol/client": "2.4.0", + "ajv": "8.20.0", + "ipaddr.js": "2.2.0" + }, + "devDependencies": { + "@types/node": "24.10.0", + "prettier": "3.6.2", + "typescript": "5.9.3" + } +} diff --git a/mcp-worker/src/index.ts b/mcp-worker/src/index.ts new file mode 100644 index 000000000..839edb2aa --- /dev/null +++ b/mcp-worker/src/index.ts @@ -0,0 +1,2 @@ +// Worker policy and service entrypoints are added after the fixture foundation. +export {}; diff --git a/mcp-worker/test/fixture.test.ts b/mcp-worker/test/fixture.test.ts new file mode 100644 index 000000000..215ec5906 --- /dev/null +++ b/mcp-worker/test/fixture.test.ts @@ -0,0 +1,531 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { test } from "node:test"; +import { + Client, + StreamableHTTPClientTransport, +} from "@modelcontextprotocol/client"; +import { startMcpFixture } from "./fixtures/server.ts"; + +for (const version of ["2026-07-28", "2025-11-25", "2025-06-18"] as const) { + for (const reply of ["json", "sse"] as const) { + test(`SDK discovers pages and calls tools over TLS: ${version} ${reply}`, async (t) => { + const fixture = await startMcpFixture({ version, reply }); + t.after(() => fixture.close()); + const client = new Client( + { name: "fixture-client", version: "1" }, + { + versionNegotiation: { mode: "auto" }, + }, + ); + t.after(() => client.close()); + const transport = new StreamableHTTPClientTransport( + new URL(fixture.url), + { fetch: fixture.fetch }, + ); + await client.connect(transport); + const first = await client.request({ method: "tools/list", params: {} }); + assert.equal(first.tools[0]?.name, "read_counter"); + assert.equal( + first.tools[0]?.inputSchema.$schema, + "http://json-schema.org/draft-07/schema#", + ); + assert.equal(first.nextCursor, "page-2"); + const second = await client.listTools({ cursor: first.nextCursor }); + assert.equal(second.tools[0]?.name, "write_counter"); + assert.equal( + second.tools[0]?.inputSchema.$schema, + "https://json-schema.org/draft/2020-12/schema", + ); + assert.equal(second.nextCursor, undefined); + const read = await client.callTool({ + name: "read_counter", + arguments: {}, + }); + assert.deepEqual(read.structuredContent, { count: 0 }); + const written = await client.callTool({ + name: "write_counter", + arguments: { delta: 2 }, + }); + assert.deepEqual(written.structuredContent, { count: 2 }); + assert.deepEqual(written.content, [{ type: "text", text: "count=2" }]); + assert.equal(written.isError, false); + assert.equal(fixture.writes, 1); + assert.equal(fixture.reads, 1); + if (version === "2026-07-28") { + assert.equal( + fixture.requests.some((r) => r.method === "initialize"), + false, + ); + assert.equal(transport.sessionId, undefined); + } else { + assert.ok(fixture.requests.some((r) => r.method === "initialize")); + assert.equal(transport.sessionId, "fixture-session-1"); + assert.equal( + fixture.requests.find((r) => r.method === "tools/call")?.sessionId, + transport.sessionId, + ); + } + }); + } +} + +test("default trust rejects the private fixture CA; explicit test trust accepts it", async (t) => { + const fixture = await startMcpFixture(); + t.after(() => fixture.close()); + await assert.rejects( + fetch(`${fixture.origin}/.well-known/oauth-protected-resource`), + ); + const response = await fixture.fetch( + `${fixture.origin}/.well-known/oauth-protected-resource`, + ); + assert.equal(response.status, 200); +}); + +test("catalog revision changes description and schema on the wire", async (t) => { + const fixture = await startMcpFixture(); + t.after(() => fixture.close()); + const before = await rpc(fixture, "tools/list"); + fixture.setCatalogRevision(2); + const after = await rpc(fixture, "tools/list"); + assert.notDeepEqual(before.body, after.body); + assert.match(JSON.stringify(after.body), /revision 2/); + assert.match(JSON.stringify(after.body), /required_tag/); +}); + +test("legacy transport sessions reject a different credential identity", async (t) => { + const fixture = await startMcpFixture({ version: "2025-11-25" }); + t.after(() => fixture.close()); + const initialized = await rpc( + fixture, + "initialize", + { + protocolVersion: "2025-11-25", + capabilities: {}, + clientInfo: { name: "test", version: "1" }, + }, + { authorization: "Bearer account-one" }, + ); + const sessionId = initialized.response.headers.get("mcp-session-id"); + assert.equal(sessionId, "fixture-session-1"); + const response = await rpc( + fixture, + "tools/list", + {}, + { + authorization: "Bearer account-two", + "mcp-session-id": sessionId!, + }, + ); + assert.equal(response.response.status, 404); +}); + +test("modern metadata header mismatches fail before a write", async (t) => { + const fixture = await startMcpFixture(); + t.after(() => fixture.close()); + const response = await rpc( + fixture, + "tools/call", + { name: "write_counter", arguments: { delta: 1 } }, + { + "mcp-method": "tools/list", + }, + ); + assert.equal(response.response.status, 400); + assert.equal(fixture.writes, 0); +}); + +test("held calls release explicitly and transport abort records cancellation", async (t) => { + const fixture = await startMcpFixture({ holdCalls: true, reply: "sse" }); + t.after(() => fixture.close()); + const controller = new AbortController(); + const pending = fixture.fetch(fixture.url, { + method: "POST", + headers: modernHeaders("tools/call", "read_counter"), + body: JSON.stringify( + message("tools/call", { name: "read_counter", arguments: {} }), + ), + signal: controller.signal, + }); + const response = await pending; + const body = response.text(); + await fixture.waitForCall(); + controller.abort(); + await assert.rejects(body); + await fixture.waitForCancellation(); + assert.equal(fixture.cancellations, 1); + assert.equal(fixture.writes, 0); + const released = rpc(fixture, "tools/call", { + name: "read_counter", + arguments: {}, + }); + await fixture.waitForCall(); + fixture.releaseCalls(); + assert.equal((await released).response.status, 200); +}); + +for (const mode of [ + "malformed", + "oversize", + "image", + "audio", + "resource", + "resource_link", + "error", +] as const) { + test(`fixture serves ${mode} output without hiding an upstream write`, async (t) => { + const fixture = await startMcpFixture({ response: mode }); + t.after(() => fixture.close()); + const response = await fixture.fetch(fixture.url, { + method: "POST", + headers: modernHeaders("tools/call", "write_counter"), + body: JSON.stringify( + message("tools/call", { + name: "write_counter", + arguments: { delta: 1 }, + }), + ), + }); + const body = await response.text(); + assert.equal(response.status, 200); + assert.equal(fixture.writes, 1); + if (mode === "malformed") assert.throws(() => JSON.parse(body)); + else if (mode === "oversize") + assert.ok(Buffer.byteLength(body) > 2 * 1024 * 1024); + else if (mode === "error") + assert.equal(JSON.parse(body).result.isError, true); + else assert.equal(JSON.parse(body).result.content[0].type, mode); + }); +} + +test("OAuth metadata, PKCE code exchange, rotation and replay use real TLS", async (t) => { + const fixture = await startMcpFixture({ requireAuth: true }); + t.after(() => fixture.close()); + const unauthorized = await rpc(fixture, "tools/list"); + assert.equal(unauthorized.response.status, 401); + assert.match( + unauthorized.response.headers.get("www-authenticate")!, + /resource_metadata=/, + ); + const resource = await ( + await fixture.fetch( + `${fixture.origin}/.well-known/oauth-protected-resource`, + ) + ).json(); + assert.equal(resource.resource, fixture.url); + assert.deepEqual(resource.authorization_servers, [fixture.origin]); + const metadata = await ( + await fixture.fetch( + `${fixture.origin}/.well-known/oauth-authorization-server`, + ) + ).json(); + assert.equal(metadata.issuer, fixture.origin); + assert.equal(metadata.authorization_response_iss_parameter_supported, true); + assert.deepEqual(metadata.code_challenge_methods_supported, ["S256"]); + const verifier = "v".repeat(43); + const authorizationUrl = new URL(metadata.authorization_endpoint); + authorizationUrl.search = new URLSearchParams({ + response_type: "code", + client_id: "fixture-public", + redirect_uri: `${fixture.origin}/callback`, + state: "state-one", + code_challenge: createHash("sha256").update(verifier).digest("base64url"), + code_challenge_method: "S256", + resource: fixture.url, + scope: "tools:read tools:write", + }).toString(); + const authorization = await fixture.fetch(authorizationUrl, { + redirect: "manual", + }); + assert.equal(authorization.status, 302); + const callback = new URL(authorization.headers.get("location")!); + assert.equal(callback.searchParams.get("iss"), metadata.issuer); + assert.equal(callback.searchParams.get("state"), "state-one"); + const code = callback.searchParams.get("code")!; + const grant = { + grant_type: "authorization_code", + code, + code_verifier: verifier, + client_id: "fixture-public", + redirect_uri: `${fixture.origin}/callback`, + resource: fixture.url, + }; + assert.equal( + (await token(fixture, { ...grant, code_verifier: "wrong" })).status, + 400, + ); + assert.equal( + (await token(fixture, { ...grant, resource: "https://other.example/mcp" })) + .status, + 400, + ); + const exchanged = await token(fixture, grant); + assert.equal(exchanged.status, 200); + const tokens = await exchanged.json(); + assert.equal(tokens.access_token, "fixture-access-1"); + assert.equal((await token(fixture, grant)).status, 400); + assert.equal((await fixture.fetch(authorizationUrl)).status, 400); + const authorized = await rpc( + fixture, + "tools/list", + {}, + { authorization: `Bearer ${tokens.access_token}` }, + ); + assert.equal(authorized.response.status, 200); + const refresh = { + grant_type: "refresh_token", + refresh_token: tokens.refresh_token, + client_id: "fixture-public", + resource: fixture.url, + }; + const rotated = await (await token(fixture, refresh)).json(); + assert.equal(rotated.refresh_token, "fixture-refresh-2"); + assert.equal((await token(fixture, refresh)).status, 400); + assert.equal( + ( + await rpc( + fixture, + "tools/list", + {}, + { authorization: `Bearer ${tokens.access_token}` }, + ) + ).response.status, + 401, + ); +}); + +test("OAuth fixture can return a mismatched callback issuer", async (t) => { + const fixture = await startMcpFixture({ + callbackIssuer: "https://other.example", + }); + t.after(() => fixture.close()); + const url = new URL(`${fixture.origin}/authorize`); + url.search = new URLSearchParams({ + response_type: "code", + client_id: "fixture-public", + redirect_uri: `${fixture.origin}/callback`, + state: "issuer-case", + resource: fixture.url, + code_challenge: "c".repeat(43), + code_challenge_method: "S256", + }).toString(); + const response = await fixture.fetch(url); + assert.equal( + new URL(response.headers.get("location")!).searchParams.get("iss"), + "https://other.example", + ); +}); + +test("OAuth rejects missing PKCE, bad redirect, unknown client, and scope expansion", async (t) => { + const fixture = await startMcpFixture(); + t.after(() => fixture.close()); + const url = new URL(`${fixture.origin}/authorize`); + const params = { + response_type: "code", + client_id: "fixture-public", + redirect_uri: `${fixture.origin}/callback`, + state: "state", + resource: fixture.url, + code_challenge: "c".repeat(43), + code_challenge_method: "S256", + scope: "tools:read", + }; + for (const change of [ + { code_challenge_method: "plain" }, + { redirect_uri: "https://other.example/callback" }, + { client_id: "unknown" }, + { scope: "admin" }, + { resource: "https://other.example" }, + ]) { + url.search = new URLSearchParams({ ...params, ...change }).toString(); + assert.equal((await fixture.fetch(url)).status, 400); + } +}); + +function message(method: string, params: Record = {}) { + return { + jsonrpc: "2.0", + id: 1, + method, + params: { + ...params, + _meta: { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientInfo": { + name: "fixture-client", + version: "1", + }, + "io.modelcontextprotocol/clientCapabilities": {}, + }, + }, + }; +} +function modernHeaders(method: string, name?: string) { + return { + "content-type": "application/json", + accept: "application/json, text/event-stream", + "mcp-protocol-version": "2026-07-28", + "mcp-method": method, + ...(name ? { "mcp-name": name } : {}), + }; +} +async function rpc( + fixture: Awaited>, + method: string, + params: Record = {}, + headers: Record = {}, +) { + const response = await fixture.fetch(fixture.url, { + method: "POST", + headers: { + ...modernHeaders( + method, + typeof params.name === "string" ? params.name : undefined, + ), + ...headers, + }, + body: JSON.stringify(message(method, params)), + }); + const text = await response.text(); + const data = text.startsWith("data: ") ? text.slice(6).trim() : text; + return { response, body: data ? JSON.parse(data) : undefined }; +} +function token( + fixture: Awaited>, + params: Record, +) { + return fixture.fetch(`${fixture.origin}/token`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams(params).toString(), + }); +} + +test("binary HTTP output retains the committed write counter", async (t) => { + const fixture = await startMcpFixture({ response: "binary" }); + t.after(() => fixture.close()); + const response = await fixture.fetch(fixture.url, { + method: "POST", + headers: modernHeaders("tools/call", "write_counter"), + body: JSON.stringify( + message("tools/call", { name: "write_counter", arguments: { delta: 1 } }), + ), + }); + assert.equal( + response.headers.get("content-type"), + "application/octet-stream", + ); + assert.deepEqual( + new Uint8Array(await response.arrayBuffer()), + new Uint8Array([0, 255, 128, 1]), + ); + assert.equal(fixture.writes, 1); +}); + +test("legacy cancellation stops only the matching held call", async (t) => { + const fixture = await startMcpFixture({ + version: "2025-11-25", + holdCalls: true, + reply: "sse", + }); + t.after(() => fixture.close()); + const initialized = await rpc(fixture, "initialize", { + protocolVersion: "2025-11-25", + capabilities: {}, + clientInfo: { name: "fixture-client", version: "1" }, + }); + const headers = { + ...modernHeaders("tools/call", "read_counter"), + "mcp-session-id": initialized.response.headers.get("mcp-session-id")!, + }; + const first = await fixture.fetch(fixture.url, { + method: "POST", + headers, + body: JSON.stringify({ + ...message("tools/call", { name: "read_counter", arguments: {} }), + id: 10, + }), + }); + const firstBody = first.text().catch(() => "canceled"); + const second = await fixture.fetch(fixture.url, { + method: "POST", + headers, + body: JSON.stringify({ + ...message("tools/call", { name: "read_counter", arguments: {} }), + id: 20, + }), + }); + const secondBody = second.text().catch(() => "canceled"); + const canceled = await fixture.fetch(fixture.url, { + method: "POST", + headers, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "notifications/cancelled", + params: { requestId: 10 }, + }), + }); + assert.equal(canceled.status, 202); + await fixture.waitForCancellation(); + fixture.releaseCalls(); + assert.equal(await firstBody, "canceled"); + assert.match(await secondBody, /"id":20/); + assert.equal(fixture.cancellations, 1); +}); + +test("legacy cancellation cannot cross a transport session with the same request ID", async (t) => { + const fixture = await startMcpFixture({ + version: "2025-11-25", + holdCalls: true, + reply: "sse", + }); + t.after(() => fixture.close()); + const firstSession = await rpc(fixture, "initialize", { + protocolVersion: "2025-11-25", + capabilities: {}, + clientInfo: { name: "fixture-client", version: "1" }, + }); + const secondSession = await rpc(fixture, "initialize", { + protocolVersion: "2025-11-25", + capabilities: {}, + clientInfo: { name: "fixture-client", version: "1" }, + }); + const firstHeaders = { + ...modernHeaders("tools/call", "read_counter"), + "mcp-session-id": firstSession.response.headers.get("mcp-session-id")!, + }; + const secondHeaders = { + ...modernHeaders("tools/call", "read_counter"), + "mcp-session-id": secondSession.response.headers.get("mcp-session-id")!, + }; + const body = JSON.stringify({ + ...message("tools/call", { name: "read_counter", arguments: {} }), + id: 10, + }); + const first = await fixture.fetch(fixture.url, { + method: "POST", + headers: firstHeaders, + body, + }); + const firstBody = first.text().catch(() => "canceled"); + const second = await fixture.fetch(fixture.url, { + method: "POST", + headers: secondHeaders, + body, + }); + const secondBody = second.text().catch(() => "canceled"); + await fixture.fetch(fixture.url, { + method: "POST", + headers: firstHeaders, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "notifications/cancelled", + params: { requestId: 10 }, + }), + }); + await fixture.waitForCancellation(); + fixture.releaseCalls(); + assert.equal(await firstBody, "canceled"); + assert.match(await secondBody, /"id":10/); + assert.equal(fixture.cancellations, 1); +}); diff --git a/mcp-worker/test/fixtures/http.ts b/mcp-worker/test/fixtures/http.ts new file mode 100644 index 000000000..4f3155e45 --- /dev/null +++ b/mcp-worker/test/fixtures/http.ts @@ -0,0 +1,52 @@ +import { request } from "node:https"; +import { TEST_CA } from "./tls.ts"; + +// Test-only HTTPS adapter: explicit private CA, streaming bodies, and abort support. +// It never changes process TLS settings and never follows redirects. +export const fixtureFetch: typeof fetch = async (input, init) => { + const source = input instanceof Request ? input : new Request(input, init); + const headers = new Headers(init?.headers ?? source.headers); + const signal = init?.signal ?? source.signal; + const body = init?.body ?? (source.body ? await source.text() : undefined); + if (body !== undefined && body !== null && typeof body !== "string") { + throw new Error("Fixture fetch accepts string request bodies only"); + } + return new Promise((resolve, reject) => { + const req = request(source.url, { + method: init?.method ?? source.method, + headers: Object.fromEntries(headers), + ca: TEST_CA, + signal, + agent: false, + }); + req.on("error", reject); + req.on("response", (res) => { + const responseHeaders = new Headers(); + for (const [key, value] of Object.entries(res.headers)) { + if (value !== undefined) + responseHeaders.set( + key, + Array.isArray(value) ? value.join(", ") : value, + ); + } + const stream = new ReadableStream({ + start(controller) { + res.on("data", (chunk: Buffer) => controller.enqueue(chunk)); + res.on("end", () => controller.close()); + res.on("error", (error) => controller.error(error)); + }, + cancel() { + res.destroy(); + }, + }); + const status = res.statusCode ?? 500; + resolve( + new Response([204, 205, 304].includes(status) ? null : stream, { + status, + headers: responseHeaders, + }), + ); + }); + req.end(body); + }); +}; diff --git a/mcp-worker/test/fixtures/oauth.ts b/mcp-worker/test/fixtures/oauth.ts new file mode 100644 index 000000000..c21b44363 --- /dev/null +++ b/mcp-worker/test/fixtures/oauth.ts @@ -0,0 +1,199 @@ +import { createHash } from "node:crypto"; +import type { IncomingMessage, ServerResponse } from "node:http"; + +interface CodeGrant { + challenge: string; + client: string; + redirect: string; + resource: string; + scope: string; +} +interface RefreshGrant { + client: string; + scope: string; +} + +// A deterministic authorization server for hostile-boundary tests, not a provider driver. +export function createOAuthFixture( + origin: () => string, + callbackIssuer?: string, +) { + const codes = new Map(); + const states = new Set(); + const refreshes = new Map(); + let codeSequence = 0; + let tokenSequence = 0; + let accessToken: string | undefined; + const resource = () => `${origin()}/mcp`; + const respond = (res: ServerResponse, value: unknown, status = 200) => { + res.writeHead(status, { + "content-type": "application/json", + "cache-control": "no-store", + }); + res.end(JSON.stringify(value)); + }; + const fail = (res: ServerResponse, error = "invalid_request") => + respond(res, { error }, 400); + return { + accepts(authorization: string | undefined) { + return ( + accessToken !== undefined && authorization === `Bearer ${accessToken}` + ); + }, + handle( + req: IncomingMessage, + res: ServerResponse, + url: URL, + body: string, + ): boolean { + if ( + url.pathname === "/.well-known/oauth-protected-resource" || + url.pathname === "/.well-known/oauth-protected-resource/mcp" + ) { + respond(res, { + resource: resource(), + authorization_servers: [origin()], + scopes_supported: ["tools:read", "tools:write"], + bearer_methods_supported: ["header"], + }); + return true; + } + if (url.pathname === "/.well-known/oauth-authorization-server") { + respond(res, { + issuer: origin(), + authorization_endpoint: `${origin()}/authorize`, + token_endpoint: `${origin()}/token`, + response_types_supported: ["code"], + grant_types_supported: ["authorization_code", "refresh_token"], + token_endpoint_auth_methods_supported: ["none", "client_secret_post"], + code_challenge_methods_supported: ["S256"], + authorization_response_iss_parameter_supported: true, + scopes_supported: ["tools:read", "tools:write"], + }); + return true; + } + if (url.pathname === "/authorize") { + const p = url.searchParams; + const client = p.get("client_id"); + const state = p.get("state"); + const challenge = p.get("code_challenge"); + const scope = p.get("scope") ?? "tools:read"; + if ( + req.method !== "GET" || + p.get("response_type") !== "code" || + !["fixture-public", "fixture-confidential"].includes(client ?? "") || + p.get("redirect_uri") !== `${origin()}/callback` || + p.get("resource") !== resource() || + p.get("code_challenge_method") !== "S256" || + !challenge || + !/^[A-Za-z0-9_-]{43}$/.test(challenge) || + !state || + states.has(state) || + !validScope(scope) + ) { + fail(res); + return true; + } + const code = `fixture-code-${++codeSequence}`; + states.add(state); + codes.set(code, { + client: client!, + challenge, + redirect: `${origin()}/callback`, + resource: resource(), + scope, + }); + const callback = new URL(`${origin()}/callback`); + callback.search = new URLSearchParams({ + code, + state, + iss: callbackIssuer ?? origin(), + }).toString(); + res.writeHead(302, { + location: callback.href, + "cache-control": "no-store", + }); + res.end(); + return true; + } + if (url.pathname !== "/token") return false; + if (req.method !== "POST") { + fail(res); + return true; + } + const p = new URLSearchParams(body); + const client = p.get("client_id"); + if ( + !["fixture-public", "fixture-confidential"].includes(client ?? "") || + (client === "fixture-confidential" && + p.get("client_secret") !== "fixture-client-secret") || + p.get("resource") !== resource() + ) { + fail(res); + return true; + } + let scope: string; + if (p.get("grant_type") === "authorization_code") { + const code = p.get("code") ?? ""; + const grant = codes.get(code); + const challenge = createHash("sha256") + .update(p.get("code_verifier") ?? "") + .digest("base64url"); + if ( + !grant || + grant.client !== client || + grant.redirect !== p.get("redirect_uri") || + grant.resource !== p.get("resource") || + grant.challenge !== challenge + ) { + fail(res, "invalid_grant"); + return true; + } + codes.delete(code); + scope = grant.scope; + } else if (p.get("grant_type") === "refresh_token") { + const refresh = p.get("refresh_token") ?? ""; + const grant = refreshes.get(refresh); + if ( + !grant || + grant.client !== client || + (p.has("scope") && !isScopeSubset(p.get("scope")!, grant.scope)) + ) { + fail(res, "invalid_grant"); + return true; + } + refreshes.delete(refresh); + scope = p.get("scope") ?? grant.scope; + } else { + fail(res, "unsupported_grant_type"); + return true; + } + ++tokenSequence; + accessToken = `fixture-access-${tokenSequence}`; + const refreshToken = `fixture-refresh-${tokenSequence}`; + refreshes.set(refreshToken, { client: client!, scope }); + respond(res, { + access_token: accessToken, + refresh_token: refreshToken, + token_type: "Bearer", + expires_in: 3600, + scope, + }); + return true; + }, + }; +} +function validScope(scope: string) { + return ( + scope.length > 0 && + scope + .split(" ") + .every((part) => ["tools:read", "tools:write"].includes(part)) + ); +} +function isScopeSubset(scope: string, previous: string) { + return ( + validScope(scope) && + scope.split(" ").every((part) => previous.split(" ").includes(part)) + ); +} diff --git a/mcp-worker/test/fixtures/server.ts b/mcp-worker/test/fixtures/server.ts new file mode 100644 index 000000000..97acaa4b1 --- /dev/null +++ b/mcp-worker/test/fixtures/server.ts @@ -0,0 +1,451 @@ +import { createServer } from "node:https"; +import type { IncomingMessage, ServerResponse } from "node:http"; +import type { Tool } from "@modelcontextprotocol/client"; +import { fixtureFetch } from "./http.ts"; +import { createOAuthFixture } from "./oauth.ts"; +import { TEST_CA, TEST_CERT, TEST_KEY } from "./tls.ts"; + +export type FixtureVersion = "2026-07-28" | "2025-11-25" | "2025-06-18"; +export type FixtureResponse = + | "normal" + | "malformed" + | "oversize" + | "binary" + | "image" + | "audio" + | "resource" + | "resource_link" + | "error"; +export interface FixtureOptions { + version?: FixtureVersion; + reply?: "json" | "sse"; + response?: FixtureResponse; + holdCalls?: boolean; + requireAuth?: boolean; + callbackIssuer?: string; +} +export interface ObservedRequest { + method: string; + sessionId: string | undefined; + protocolVersion: string | undefined; +} +interface RpcMessage { + jsonrpc: "2.0"; + id?: number | string; + method: string; + params?: Record; +} + +export async function startMcpFixture(options: FixtureOptions = {}) { + const version = options.version ?? "2026-07-28"; + const modern = version === "2026-07-28"; + let origin = ""; + let revision = 1; + let count = 0; + let reads = 0; + let writes = 0; + let cancellations = 0; + const requests: ObservedRequest[] = []; + const sessions = new Map(); + const pending = new Map< + ServerResponse, + { + id: number | string | undefined; + sessionId: string | undefined; + complete: () => void; + } + >(); + const calls = eventBarrier(); + const canceled = eventBarrier(); + const oauth = createOAuthFixture(() => origin, options.callbackIssuer); + const server = createServer( + { key: TEST_KEY, cert: TEST_CERT }, + (req, res) => { + void handle(req, res).catch((error: unknown) => { + if (!res.headersSent) res.writeHead(500); + res.end( + error instanceof Error ? error.message : "Fixture request failed", + ); + }); + }, + ); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("Missing fixture listener"); + origin = `https://127.0.0.1:${address.port}`; + + async function handle(req: IncomingMessage, res: ServerResponse) { + const url = new URL(req.url ?? "/", origin); + const chunks: Buffer[] = []; + for await (const chunk of req) chunks.push(Buffer.from(chunk)); + const raw = Buffer.concat(chunks).toString("utf8"); + if (oauth.handle(req, res, url, raw)) return; + if (url.pathname !== "/mcp") { + res.writeHead(404).end(); + return; + } + if (options.requireAuth && !oauth.accepts(req.headers.authorization)) { + res.writeHead(401, { + "content-type": "application/json", + "www-authenticate": `Bearer resource_metadata="${origin}/.well-known/oauth-protected-resource"`, + }); + res.end(JSON.stringify({ error: "unauthorized" })); + return; + } + if (req.method === "GET") { + res.writeHead(405).end(); + return; + } + const sessionId = header(req, "mcp-session-id"); + if ( + req.method === "DELETE" && + !modern && + sessionId && + sessions.has(sessionId) + ) { + sessions.delete(sessionId); + res.writeHead(200).end(); + return; + } + if (req.method !== "POST") { + res.writeHead(405).end(); + return; + } + let body: unknown; + try { + body = JSON.parse(raw); + } catch { + res.writeHead(400).end(); + return; + } + if (!isRpcMessage(body)) { + res.writeHead(400).end(); + return; + } + const message = body; + requests.push({ + method: message.method, + sessionId, + protocolVersion: header(req, "mcp-protocol-version"), + }); + const rpcError = (code: number, text: string, status = 200) => + send( + res, + { + jsonrpc: "2.0", + id: message.id ?? null, + error: { code, message: text }, + }, + status, + ); + if (!modern && message.method === "server/discover") { + rpcError(-32601, "Method not found"); + return; + } + if (!modern && message.method === "initialize") { + const id = `fixture-session-${sessions.size + 1}`; + sessions.set(id, req.headers.authorization); + res.setHeader("mcp-session-id", id); + sendResult(res, message, { + protocolVersion: version, + capabilities: { tools: { listChanged: true } }, + serverInfo: { name: "mcp-fixture", version: "1" }, + }); + return; + } + if ( + !modern && + (!sessionId || + !sessions.has(sessionId) || + sessions.get(sessionId) !== req.headers.authorization) + ) { + rpcError(-32001, "Unknown session", 404); + return; + } + if (message.method === "notifications/initialized") { + res.writeHead(202).end(); + return; + } + if (message.method === "notifications/cancelled") { + for (const [response, call] of pending) { + if ( + call.id === message.params?.requestId && + call.sessionId === sessionId + ) + response.destroy(); + } + res.writeHead(202).end(); + return; + } + if (modern && !validModernHeaders(req, message)) { + rpcError(-32021, "Request metadata mismatch", 400); + return; + } + switch (message.method) { + case "server/discover": + sendResult(res, message, { + supportedVersions: [version], + capabilities: { tools: {} }, + _meta: { + "io.modelcontextprotocol/serverInfo": { + name: "mcp-fixture", + version: "1", + }, + }, + }); + return; + case "ping": + sendResult(res, message, {}); + return; + case "tools/list": { + const page = message.params?.cursor; + if (page !== undefined && page !== "page-2") { + rpcError(-32602, "Unknown cursor"); + return; + } + sendResult(res, message, { + tools: [ + catalogTool( + page === "page-2" ? "write_counter" : "read_counter", + revision, + ), + ], + ...(page === "page-2" ? {} : { nextCursor: "page-2" }), + }); + return; + } + case "tools/call": { + const args = message.params?.arguments; + if (!isObject(args)) { + rpcError(-32602, "Arguments must be an object"); + return; + } + if (message.params?.name === "read_counter") ++reads; + else if ( + message.params?.name === "write_counter" && + Number.isSafeInteger(args.delta) + ) { + ++writes; + count += Number(args.delta); + } else { + rpcError(-32602, "Unknown tool or invalid delta"); + return; + } + const complete = () => toolReply(res, message, count); + if (options.holdCalls) { + if (options.reply === "sse") { + res.writeHead(200, { "content-type": "text/event-stream" }); + res.flushHeaders(); + } + pending.set(res, { id: message.id, sessionId, complete }); + res.on("close", () => { + if (pending.delete(res) && !res.writableEnded) { + ++cancellations; + canceled.signal(); + } + }); + } + calls.signal(); + if (!options.holdCalls) complete(); + return; + } + default: + rpcError(-32601, "Method not found"); + } + } + function send(res: ServerResponse, body: unknown, status = 200) { + const json = JSON.stringify(body); + if (options.reply === "sse" && status === 200) { + if (!res.headersSent) + res.writeHead(status, { "content-type": "text/event-stream" }); + res.end(`data: ${json}\n\n`); + } else { + res.writeHead(status, { "content-type": "application/json" }); + res.end(json); + } + } + function sendResult( + res: ServerResponse, + message: RpcMessage, + result: unknown, + ) { + send(res, { + jsonrpc: "2.0", + id: message.id, + result: + modern && isObject(result) + ? { + ...result, + resultType: "complete", + ...(message.method === "tools/list" + ? { ttlMs: 0, cacheScope: "private" } + : {}), + } + : result, + }); + } + function toolReply(res: ServerResponse, message: RpcMessage, value: number) { + if (options.response === "binary") { + res.writeHead(200, { "content-type": "application/octet-stream" }); + res.end(Buffer.from([0, 255, 128, 1])); + return; + } + if (options.response === "malformed") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("{broken"); + return; + } + let content: unknown[] = [{ type: "text", text: `count=${value}` }]; + switch (options.response) { + case "oversize": + content = [{ type: "text", text: "x".repeat(2 * 1024 * 1024 + 1) }]; + break; + case "image": + content = [{ type: "image", data: "AA==", mimeType: "image/png" }]; + break; + case "audio": + content = [{ type: "audio", data: "AA==", mimeType: "audio/wav" }]; + break; + case "resource": + content = [ + { + type: "resource", + resource: { + uri: "https://resource.example/data", + text: "untrusted", + }, + }, + ]; + break; + case "resource_link": + content = [ + { + type: "resource_link", + uri: "https://resource.example/data", + name: "untrusted", + }, + ]; + break; + } + sendResult(res, message, { + content, + structuredContent: { count: value }, + isError: options.response === "error", + }); + } + return { + get url() { + return `${origin}/mcp`; + }, + get origin() { + return origin; + }, + ca: TEST_CA, + fetch: fixtureFetch, + requests, + get reads() { + return reads; + }, + get writes() { + return writes; + }, + get cancellations() { + return cancellations; + }, + setCatalogRevision(value: number) { + revision = value; + }, + waitForCall: calls.wait, + waitForCancellation: canceled.wait, + releaseCalls() { + const held = [...pending.values()]; + pending.clear(); + for (const call of held) call.complete(); + }, + async close() { + for (const [res] of pending) res.destroy(); + pending.clear(); + server.closeAllConnections(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + }, + }; +} +function header(req: IncomingMessage, name: string): string | undefined { + const value = req.headers[name]; + return typeof value === "string" ? value : undefined; +} +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function isRpcMessage(value: unknown): value is RpcMessage { + return ( + isObject(value) && + value.jsonrpc === "2.0" && + typeof value.method === "string" && + (value.id === undefined || + typeof value.id === "number" || + typeof value.id === "string") && + (value.params === undefined || isObject(value.params)) + ); +} +function validModernHeaders(req: IncomingMessage, message: RpcMessage) { + const meta = message.params?._meta; + return ( + isObject(meta) && + meta["io.modelcontextprotocol/protocolVersion"] === "2026-07-28" && + header(req, "mcp-protocol-version") === "2026-07-28" && + header(req, "mcp-method") === message.method && + (message.method !== "tools/call" || + header(req, "mcp-name") === message.params?.name) + ); +} +function catalogTool(name: string, revision: number): Tool { + return { + name, + description: `Counter tool revision ${revision}`, + inputSchema: { + type: "object", + $schema: + name === "read_counter" + ? "http://json-schema.org/draft-07/schema#" + : "https://json-schema.org/draft/2020-12/schema", + properties: { + ...(name === "write_counter" ? { delta: { type: "integer" } } : {}), + ...(revision > 1 ? { required_tag: { type: "string" } } : {}), + }, + required: [ + ...(name === "write_counter" ? ["delta"] : []), + ...(revision > 1 ? ["required_tag"] : []), + ], + additionalProperties: false, + }, + annotations: { + readOnlyHint: name === "read_counter", + destructiveHint: name === "write_counter", + }, + }; +} +function eventBarrier() { + let available = 0; + const waiters: (() => void)[] = []; + return { + signal() { + const waiter = waiters.shift(); + if (waiter) waiter(); + else ++available; + }, + wait() { + if (available > 0) { + --available; + return Promise.resolve(); + } + return new Promise((resolve) => waiters.push(resolve)); + }, + }; +} diff --git a/mcp-worker/test/fixtures/tls.ts b/mcp-worker/test/fixtures/tls.ts new file mode 100644 index 000000000..0035d94eb --- /dev/null +++ b/mcp-worker/test/fixtures/tls.ts @@ -0,0 +1,69 @@ +// Public test credentials. Never import this module from production code. +export const TEST_CA = `-----BEGIN CERTIFICATE----- +MIIC/DCCAeSgAwIBAgIUfAw9gD43biKgT5RrLP/+JNGoikMwDQYJKoZIhvcNAQEL +BQAwHjEcMBoGA1UEAwwTTUNQIGZpeHR1cmUgdGVzdCBDQTAeFw0yNjExMDIwMTQ2 +MTBaFw0zNjEwMzAwMTQ2MTBaMB4xHDAaBgNVBAMME01DUCBmaXh0dXJlIHRlc3Qg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCoUOEVWF/JV91busqi +/EYyOp2m+Y5jvVHioE5HWTUFFR7IwF3Cvgb2OrXvThSDoeoO/eijbybYzudMzfH5 +UB3C+Kc0V9SQl06qIEGXa1uWP5peOsIrGkmSnX4O0t7LJ9NBoLDXqPb1tCw4HPdP +Fr6H4VaJTQaNYNBHNBkm47TRYUE79RsyZ5ODErbiN/ZN+nT/Ql0S5Y3KJdyJykn3 +JFAA9J/Jepwlmut7qZzWGokwmNm2vMoXtarMsLj/Ka0R+Po/KhFgvBUnuelFcCS4 +DQD1YpO61ALTi50aTznoTitB93/nTK6Vn0cFb2cJcWPwF5H7UnKGejTZLLKIh3/k +D6yfAgMBAAGjMjAwMB0GA1UdDgQWBBQv02S/NFnklRHjU1G2w9LFmjG03DAPBgNV +HRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCVnj6//yvrGjVxVIqjtuVl +Mft3Av5oHkTbmjhlHIS0HtAq3Z+w35IkGWVAMdcVsxzsM/jN5NNHWBJEjdmppCUz +T2tYm0usD9jnqxXm6UgnrlD+wO9hauEOZoyEJT+2rbUqmrlh6PW7D0LELboHd8/j +jFK1mzZ0s8giWHCukUNeFVPwVP7kMoeydBo3AJG2eerH30wjQshOjyCjMiqZ5vGk +Z13z8ZIQzs30Dct6UO8YvU+h33S/+CDC585IwBJgf0wGv6+7WoFKUvaTg/UXPezZ +lOdOQC+aZ8oCX6K9Z3vf/1tiOXeBydROxp25CVsGAqK8SnbpeZNvqk6NahnvlYGK +-----END CERTIFICATE----- +`; +export const TEST_CERT = `-----BEGIN CERTIFICATE----- +MIIDCzCCAfOgAwIBAgIUdlie7U+wrUMgQWBB/ag6VDSSzcEwDQYJKoZIhvcNAQEL +BQAwHjEcMBoGA1UEAwwTTUNQIGZpeHR1cmUgdGVzdCBDQTAeFw0yNjExMDIwMTQ2 +MTBaFw0zNjEwMzAwMTQ2MTBaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKneoKMRRkFVDy5TL82Y9yW8z8gT54J6 +7hPNp/Ez3EPMPqQsB9Z9NMYms4pLZ4HTO71ZdPtQ5doi2MRGHvbkLZRWZZnWHHK3 +D6DkP+5kExqiCIuU3bSK90bv1L6cIFK4bBV0MyYRfLULtmQ/5mHWdCGuifmlKASO +NlruI5/tZRHM0pdLTGgy/FylIc3IQsLBQcIbvQMmGLnTxRUwv4ulRo4SruUm02I9 +Vlm/WnKXxdOZNa51tbldouMJ3L1iy1chXK45bBrMhNxAKaAvEzo96MDubKou9PN/ +pmIr9ahyIhwa3oxt80VAwMRdXikdGq9c83DrDpLBbBF7OJBF158+hcECAwEAAaNL +MEkwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMAkGA1UdEwQCMAAwCwYDVR0P +BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4IBAQBG +kU7MiyRbKFT4Zy88epm23cGyS5c96k9Bip2ZgSrKaiqfgWrMZ5wjIKq2gP7zgKG4 +LY56AGPPc5BYEk+1NQwnhcTsN2omYHXPLEnoJAqwZpJ341jY6SpfexbFWAqVBOhq +GvOfxapMblv26UYOYzkiLE9Fmr1SpAkU9ajAFIjdzwE2A//CVgV8AHv4cUTD+ILL +h73/m9eEvJmxfBz2Ee5PxaVtHVPgXZIZHR3ir5v2yEJVIGfJvh5Tui9/5RG4UxVV +icT+7k45Ut1PFLTRIuezQ18X0hzs+TV5Gug0YMsF9U23OVAWfekp81RIWC9aGak5 +ymz4n6rOKfDjvlvT+dcW +-----END CERTIFICATE----- +`; +export const TEST_KEY = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCp3qCjEUZBVQ8u +Uy/NmPclvM/IE+eCeu4TzafxM9xDzD6kLAfWfTTGJrOKS2eB0zu9WXT7UOXaItjE +Rh725C2UVmWZ1hxytw+g5D/uZBMaogiLlN20ivdG79S+nCBSuGwVdDMmEXy1C7Zk +P+Zh1nQhron5pSgEjjZa7iOf7WURzNKXS0xoMvxcpSHNyELCwUHCG70DJhi508UV +ML+LpUaOEq7lJtNiPVZZv1pyl8XTmTWudbW5XaLjCdy9YstXIVyuOWwazITcQCmg +LxM6PejA7myqLvTzf6ZiK/WociIcGt6MbfNFQMDEXV4pHRqvXPNw6w6SwWwReziQ +RdefPoXBAgMBAAECggEAG/L8XTg3ZgJ3x/h58gsMBxKE4Xec7NQLHgkFa+NLh4Aw +glSYSYs6RqwReFylwC+gMaxpWfxahUScg9bBu3+ZnEGG4APhlGJYWPGVL0UZ+5TT +cRviSpVe0DYfu+j2TIX5iqIe1FGmQieWL/JvloNxgiU0oHWzGuTHPrjV9bdanM3g +4w2HdJS0nje5jNKCVcXeejZ2BPx2udF1P0qZZxVVkcbHOFuN9/6QXB8lXo6hlmrQ +xZXKAlwdzECpn1o11pgknZU7T3GGHN2WsYEniQv4hHjQEg+RNqrx0UK6QDAIon3y +pE7CEQpRDMsmL0GH0u+v6KdMar1AIWQWOCWHUNUyUQKBgQDXr+0ZKaEyw15umpuh +ulg3raCoB6XxS3ohw7CYz0FDbjjFZeXr5yrW0bzyBJJYlAWZFwW+wtIA151s64HE +wuuAGHPs2n0pO1PyQQlRLInZTK7mKkLuyIaWCBdq3QX9l3FNMqkVtSN9Co40Xkop ++315FIfFA8SR3P1FM/eOSehNlQKBgQDJnnMCT7r/TYi0qKrhXU21G9eMRUzVKVqu +VxVY9K/fiK79Ak9Cg8XISAn4PIt3JbBz3birZEYRYan6BCqkagv0JIe5r4p1wh6t +MkfNQSl4ZyKen6YNeXsbIB2HI/hKWVhGcQaYezB4DGxmoYuGc0LoxcUBAULqVXB/ +qi65PX0UfQKBgQCymnaumH9opslCs3peoQ0+BeP8YGD0f7jBzuckkrMLYHFYXLnY +79LUkldw3YJGIUTFcYALaA44LlOpBxetVG9o4lbUclSkWQF5oItTkZsQuuQ1qhdE +3f48rLSfhCjaxpZHA8VqOVYKzDvXd6DYRO2w3uRSq30SADomHRNI20CnRQKBgHsN +sQYFIY12hew1UW4Q4z+ZSZo8JaOoBMeJMOdFGB5nLt4Ozc8727fcd/FBCqHip+pf +tbktjzJGmFaWumv0qW0N6qSjBe/PVafBm18j2Sq6T7/ekpuAvGkW1c7IXl8KXr1H +eI4SCcgLVRHioOSjbn/hTNbLLUsWcH4KoU+/vpqFAoGBAIbz/H/q+lcKXBaZYzxJ +TdYJZrH1cdOIecqlWYqZyO75VfJXuikcZAh8fRg2hMF4MpvLEt9H6akKStji/gSD +ICTOEFZsc8szg0mdSTCI7IVBYrgsImdKYtsFVRhwf+vhypAne5yvau7Y+zw1xWct +DrSTp+bphObufZ4IzPYHPThU +-----END PRIVATE KEY----- +`; diff --git a/mcp-worker/tsconfig.build.json b/mcp-worker/tsconfig.build.json new file mode 100644 index 000000000..98a8d519f --- /dev/null +++ b/mcp-worker/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "rewriteRelativeImportExtensions": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true + }, + "include": ["src/**/*.ts"] +} diff --git a/mcp-worker/tsconfig.json b/mcp-worker/tsconfig.json new file mode 100644 index 000000000..f268372b1 --- /dev/null +++ b/mcp-worker/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} From 358cb521dcb96b91265a89fe1ef3b024a88d5d0c Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 20:58:30 -0500 Subject: [PATCH 002/279] Fix legacy MCP fixture session lifetime and termination --- mcp-worker/test/fixture.test.ts | 99 ++++++++++++++++++++++++++++++ mcp-worker/test/fixtures/server.ts | 18 +++--- 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/mcp-worker/test/fixture.test.ts b/mcp-worker/test/fixture.test.ts index 215ec5906..3caceb02c 100644 --- a/mcp-worker/test/fixture.test.ts +++ b/mcp-worker/test/fixture.test.ts @@ -529,3 +529,102 @@ test("legacy cancellation cannot cross a transport session with the same request assert.match(await secondBody, /"id":10/); assert.equal(fixture.cancellations, 1); }); + +for (const version of ["2025-11-25", "2025-06-18"] as const) { + test(`legacy session IDs remain distinct after termination: ${version}`, async (t) => { + const fixture = await startMcpFixture({ version }); + t.after(() => fixture.close()); + const initialize = (authorization: string) => + rpc( + fixture, + "initialize", + { + protocolVersion: version, + capabilities: {}, + clientInfo: { name: "test", version: "1" }, + }, + { authorization }, + ); + const first = await initialize("Bearer account-one"); + const second = await initialize("Bearer account-two"); + const firstId = first.response.headers.get("mcp-session-id")!; + const secondId = second.response.headers.get("mcp-session-id")!; + const deleted = await fixture.fetch(fixture.url, { + method: "DELETE", + headers: { + authorization: "Bearer account-one", + "mcp-session-id": firstId, + }, + }); + assert.equal(deleted.status, 200); + const third = await initialize("Bearer account-three"); + const thirdId = third.response.headers.get("mcp-session-id"); + assert.notEqual(thirdId, firstId); + assert.notEqual(thirdId, secondId); + assert.equal(thirdId, "fixture-session-3"); + const stillActive = await rpc( + fixture, + "tools/list", + {}, + { + authorization: "Bearer account-two", + "mcp-session-id": secondId, + }, + ); + assert.equal(stillActive.response.status, 200); + assert.equal(stillActive.body.result.tools[0].name, "read_counter"); + }); + + test(`legacy session termination requires its credential identity: ${version}`, async (t) => { + const fixture = await startMcpFixture({ version }); + t.after(() => fixture.close()); + const initialized = await rpc( + fixture, + "initialize", + { + protocolVersion: version, + capabilities: {}, + clientInfo: { name: "test", version: "1" }, + }, + { authorization: "Bearer account-one" }, + ); + const sessionId = initialized.response.headers.get("mcp-session-id")!; + const denied = await fixture.fetch(fixture.url, { + method: "DELETE", + headers: { + authorization: "Bearer account-two", + "mcp-session-id": sessionId, + }, + }); + assert.equal(denied.status, 404); + const stillActive = await rpc( + fixture, + "tools/list", + {}, + { + authorization: "Bearer account-one", + "mcp-session-id": sessionId, + }, + ); + assert.equal(stillActive.response.status, 200); + assert.equal(stillActive.body.result.tools[0].name, "read_counter"); + const deleted = await fixture.fetch(fixture.url, { + method: "DELETE", + headers: { + authorization: "Bearer account-one", + "mcp-session-id": sessionId, + }, + }); + assert.equal(deleted.status, 200); + const terminated = await rpc( + fixture, + "tools/list", + {}, + { + authorization: "Bearer account-one", + "mcp-session-id": sessionId, + }, + ); + assert.equal(terminated.response.status, 404); + }); +} diff --git a/mcp-worker/test/fixtures/server.ts b/mcp-worker/test/fixtures/server.ts index 97acaa4b1..c81df1966 100644 --- a/mcp-worker/test/fixtures/server.ts +++ b/mcp-worker/test/fixtures/server.ts @@ -45,6 +45,7 @@ export async function startMcpFixture(options: FixtureOptions = {}) { let reads = 0; let writes = 0; let cancellations = 0; + let sessionSequence = 0; const requests: ObservedRequest[] = []; const sessions = new Map(); const pending = new Map< @@ -101,12 +102,15 @@ export async function startMcpFixture(options: FixtureOptions = {}) { return; } const sessionId = header(req, "mcp-session-id"); - if ( - req.method === "DELETE" && - !modern && - sessionId && - sessions.has(sessionId) - ) { + if (req.method === "DELETE" && !modern) { + if ( + !sessionId || + !sessions.has(sessionId) || + sessions.get(sessionId) !== req.headers.authorization + ) { + res.writeHead(404).end(); + return; + } sessions.delete(sessionId); res.writeHead(200).end(); return; @@ -147,7 +151,7 @@ export async function startMcpFixture(options: FixtureOptions = {}) { return; } if (!modern && message.method === "initialize") { - const id = `fixture-session-${sessions.size + 1}`; + const id = `fixture-session-${++sessionSequence}`; sessions.set(id, req.headers.authorization); res.setHeader("mcp-session-id", id); sendResult(res, message, { From 76a20cee7834ed16d30554d9da1f0ca7c7abf32a Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 21:20:00 -0500 Subject: [PATCH 003/279] Add durable MCP authority and typed administration contracts --- .../proto/engram/app/v1/integration.proto | 174 +- .../proto/engram/app/v1/profile.proto | 8 + orchestrator/drizzle/0101_mcp_authority.sql | 138 + .../drizzle/0102_mcp_authorization_kind.sql | 3 + .../drizzle/0103_mcp_session_retention.sql | 12 + orchestrator/drizzle/meta/0101_snapshot.json | 8410 ++++++++++++++++ orchestrator/drizzle/meta/0102_snapshot.json | 8426 +++++++++++++++++ orchestrator/drizzle/meta/0103_snapshot.json | 8426 +++++++++++++++++ orchestrator/drizzle/meta/_journal.json | 21 + orchestrator/src/__tests__/mcp-domain.test.ts | 165 + orchestrator/src/__tests__/mcp-rpc.test.ts | 212 + orchestrator/src/__tests__/mcp-store.test.ts | 517 + orchestrator/src/__tests__/profiles.test.ts | 19 + .../src/db/integration-connections.ts | 23 +- orchestrator/src/db/schema.ts | 180 + .../src/gen/engram/app/v1/integration_pb.ts | 682 +- .../src/gen/engram/app/v1/profile_pb.ts | 39 +- orchestrator/src/integrations/grants.ts | 12 + orchestrator/src/mcp/approvals.ts | 8 + orchestrator/src/mcp/bindings.ts | 127 + orchestrator/src/mcp/catalog.ts | 110 + orchestrator/src/mcp/config.ts | 45 + orchestrator/src/mcp/contracts.ts | 334 + orchestrator/src/mcp/hash.ts | 31 + orchestrator/src/mcp/journal.ts | 560 ++ orchestrator/src/mcp/limits.ts | 22 + orchestrator/src/mcp/repository.ts | 293 + orchestrator/src/mcp/types.ts | 73 + orchestrator/src/rpc/integration.ts | 40 + orchestrator/src/rpc/profiles.ts | 18 +- orchestrator/src/rpc/task-create.ts | 5 + ...gration-IntegrationService_connectquery.ts | 45 + web/src/gen/engram/app/v1/integration_pb.ts | 682 +- web/src/gen/engram/app/v1/profile_pb.ts | 39 +- 34 files changed, 29875 insertions(+), 24 deletions(-) create mode 100644 orchestrator/drizzle/0101_mcp_authority.sql create mode 100644 orchestrator/drizzle/0102_mcp_authorization_kind.sql create mode 100644 orchestrator/drizzle/0103_mcp_session_retention.sql create mode 100644 orchestrator/drizzle/meta/0101_snapshot.json create mode 100644 orchestrator/drizzle/meta/0102_snapshot.json create mode 100644 orchestrator/drizzle/meta/0103_snapshot.json create mode 100644 orchestrator/src/__tests__/mcp-domain.test.ts create mode 100644 orchestrator/src/__tests__/mcp-rpc.test.ts create mode 100644 orchestrator/src/__tests__/mcp-store.test.ts create mode 100644 orchestrator/src/mcp/approvals.ts create mode 100644 orchestrator/src/mcp/bindings.ts create mode 100644 orchestrator/src/mcp/catalog.ts create mode 100644 orchestrator/src/mcp/config.ts create mode 100644 orchestrator/src/mcp/contracts.ts create mode 100644 orchestrator/src/mcp/hash.ts create mode 100644 orchestrator/src/mcp/journal.ts create mode 100644 orchestrator/src/mcp/limits.ts create mode 100644 orchestrator/src/mcp/repository.ts create mode 100644 orchestrator/src/mcp/types.ts diff --git a/crates/engram-protocol/proto/engram/app/v1/integration.proto b/crates/engram-protocol/proto/engram/app/v1/integration.proto index 6387547a7..ebd22df48 100644 --- a/crates/engram-protocol/proto/engram/app/v1/integration.proto +++ b/crates/engram-protocol/proto/engram/app/v1/integration.proto @@ -52,6 +52,15 @@ service IntegrationService { rpc TestConnection(TestConnectionRequest) returns (TestConnectionResponse); rpc SetConnectionEnabled(SetConnectionEnabledRequest) returns (SetConnectionEnabledResponse); rpc GetGoogleCloudSetup(GetGoogleCloudSetupRequest) returns (GetGoogleCloudSetupResponse); + rpc DiscoverMcp(DiscoverMcpRequest) returns (DiscoverMcpResponse); + rpc PublishMcp(PublishMcpRequest) returns (PublishMcpResponse); + rpc AuthorizeMcp(AuthorizeMcpRequest) returns (AuthorizeMcpResponse); + rpc DisconnectMcp(DisconnectMcpRequest) returns (DisconnectMcpResponse); + rpc ListMcpApprovals(ListMcpApprovalsRequest) returns (ListMcpApprovalsResponse); + rpc DecideMcpApproval(DecideMcpApprovalRequest) returns (DecideMcpApprovalResponse); + rpc CancelMcpCall(CancelMcpCallRequest) returns (CancelMcpCallResponse); + rpc ResolveMcpOutcome(ResolveMcpOutcomeRequest) returns (ResolveMcpOutcomeResponse); + rpc RebindMcp(RebindMcpRequest) returns (RebindMcpResponse); } message GoogleCloudConnectionConfig { @@ -78,37 +87,69 @@ message IntegrationConnection { string updated_at = 8; GoogleCloudConnectionConfig google_cloud = 9; bool is_default = 10; + McpConnectionConfig mcp = 11; + uint32 config_revision = 12; + uint32 auth_generation = 13; + string credential_identity = 14; + uint32 catalog_revision = 15; + bool revoked = 16; } message ListConnectionsRequest {} -message ListConnectionsResponse { repeated IntegrationConnection connections = 1; } +message ListConnectionsResponse { + repeated IntegrationConnection connections = 1; +} message CreateConnectionRequest { string alias = 1; string provider = 2; string display_name = 3; GoogleCloudConnectionConfig google_cloud = 4; + McpConnectionConfig mcp = 5; +} +message CreateConnectionResponse { + IntegrationConnection connection = 1; } -message CreateConnectionResponse { IntegrationConnection connection = 1; } message UpdateConnectionRequest { + uint32 expected_revision = 6; string id = 1; string alias = 2; string display_name = 3; GoogleCloudConnectionConfig google_cloud = 4; + McpConnectionConfig mcp = 5; +} +message UpdateConnectionResponse { + IntegrationConnection connection = 1; } -message UpdateConnectionResponse { IntegrationConnection connection = 1; } -message DeleteConnectionRequest { string id = 1; } -message DeleteConnectionResponse { bool deleted = 1; } +message DeleteConnectionRequest { + string id = 1; +} +message DeleteConnectionResponse { + bool deleted = 1; +} -message TestConnectionRequest { string id = 1; } -message TestConnectionResponse { bool ok = 1; string message = 2; } +message TestConnectionRequest { + string id = 1; +} +message TestConnectionResponse { + bool ok = 1; + string message = 2; +} -message SetConnectionEnabledRequest { string id = 1; bool enabled = 2; } -message SetConnectionEnabledResponse { IntegrationConnection connection = 1; } +message SetConnectionEnabledRequest { + string id = 1; + bool enabled = 2; + uint32 expected_revision = 3; +} +message SetConnectionEnabledResponse { + IntegrationConnection connection = 1; +} -message GetGoogleCloudSetupRequest { string id = 1; } +message GetGoogleCloudSetupRequest { + string id = 1; +} message GetGoogleCloudSetupResponse { string issuer = 1; string audience = 2; @@ -184,9 +225,9 @@ message ConnectorDisplay { // One grantable power, derived for display (the slug stays canonical). message CatalogCapability { - string action = 1; // e.g. "issues:write" - string access = 2; // "read" | "write" (derived from the op's HTTP method) - string asset = 3; // asset kind this op surfaces, or "" when none + string action = 1; // e.g. "issues:write" + string access = 2; // "read" | "write" (derived from the op's HTTP method) + string asset = 3; // asset kind this op surfaces, or "" when none // ADR 0109 seam: operator-facing name. A named-connection provider serves // its operation list here so the web renders from the SAME table the // orchestrator compiles policy from — two hand-maintained copies drift, and @@ -239,7 +280,7 @@ message GetIntegrationCatalogResponse { message SetMintCredentialRequest { string provider = 1; // e.g. "github" - string kind = 2; // e.g. "github_app" + string kind = 2; // e.g. "github_app" // field name → value (e.g. app_id, private_key_pem). Each non-empty value is // sealed as the org secret `.`; values are never returned. map values = 3; @@ -291,3 +332,108 @@ message SetConnectorSettingsRequest { message SetConnectorSettingsResponse { Connector connector = 1; } + +// Organization credential references only. Secret values never enter this contract. +message McpConnectionConfig { + string endpoint = 1; + string zone = 2; + string auth_kind = 3; + string api_key_secret_ref = 4; + string oauth_client_registration_id = 5; + bool share_with_org_sessions = 6; +} +message McpTool { + string upstream_name = 1; + string description = 2; + string input_schema_json = 3; + string output_schema_json = 4; + // Empty in discovery drafts. Publication requires explicit read or write. + string classification = 5; + bool enabled = 6; + string fingerprint = 7; +} +message McpCatalog { + string id = 1; + string connection_id = 2; + string kind = 3; + uint32 revision = 4; + uint32 config_revision = 5; + uint32 auth_generation = 6; + repeated McpTool tools = 7; + string fingerprint = 8; +} +message DiscoverMcpRequest { + string connection_id = 1; + uint32 expected_revision = 2; +} +message DiscoverMcpResponse { + McpCatalog catalog = 1; +} +message PublishMcpRequest { + string connection_id = 1; + string draft_id = 2; + uint32 expected_revision = 3; + repeated McpTool tools = 4; +} +message PublishMcpResponse { + McpCatalog catalog = 1; +} +message AuthorizeMcpRequest { + string connection_id = 1; + uint32 expected_generation = 2; + bool share_with_org_sessions = 3; +} +message AuthorizeMcpResponse { + string authorization_url = 1; +} +message DisconnectMcpRequest { + string connection_id = 1; + uint32 expected_generation = 2; +} +message DisconnectMcpResponse { + IntegrationConnection connection = 1; +} +message McpApproval { + string execution_id = 1; + string session_id = 2; + string run_id = 3; + string tool_call_id = 4; + string connection_id = 5; + string upstream_name = 6; + string request_digest = 7; + string argument_hash = 8; + string expires_at = 9; +} +message ListMcpApprovalsRequest { + string session_id = 1; +} +message ListMcpApprovalsResponse { + repeated McpApproval approvals = 1; +} +message DecideMcpApprovalRequest { + string execution_id = 1; + bool approve = 2; +} +message DecideMcpApprovalResponse { + string state = 1; +} +message CancelMcpCallRequest { + string execution_id = 1; +} +message CancelMcpCallResponse { + string state = 1; +} +message ResolveMcpOutcomeRequest { + string execution_id = 1; + string outcome = 2; +} +message ResolveMcpOutcomeResponse { + string outcome = 1; +} +message RebindMcpRequest { + string session_id = 1; + string expected_binding_hash = 2; +} +message RebindMcpResponse { + string binding_hash = 1; +} diff --git a/crates/engram-protocol/proto/engram/app/v1/profile.proto b/crates/engram-protocol/proto/engram/app/v1/profile.proto index 7ac6c19f7..0a5864120 100644 --- a/crates/engram-protocol/proto/engram/app/v1/profile.proto +++ b/crates/engram-protocol/proto/engram/app/v1/profile.proto @@ -144,6 +144,7 @@ message ProfileIntegrationGrant { // user's personal credential (human launches then require it; programmatic // sessions always use the org credential). string credential_scope = 4; + McpToolGrant mcp = 5; } // ADR 0057: a profile's egress network allow-list. `default` is the posture for @@ -276,3 +277,10 @@ message DeleteProfileRequest { } message DeleteProfileResponse {} + +message McpToolGrant { + string upstream_name = 1; + bool automation_write = 2; + string argument_constraints_json = 3; + bool unrestricted = 4; +} diff --git a/orchestrator/drizzle/0101_mcp_authority.sql b/orchestrator/drizzle/0101_mcp_authority.sql new file mode 100644 index 000000000..0fbe12193 --- /dev/null +++ b/orchestrator/drizzle/0101_mcp_authority.sql @@ -0,0 +1,138 @@ +CREATE TABLE "mcp_admission" ( + "execution_id" text PRIMARY KEY NOT NULL, + "deployment_id" text NOT NULL, + "connection_id" text NOT NULL, + "session_id" text NOT NULL, + "state" text NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone NOT NULL, + CONSTRAINT "mcp_admission_state_check" CHECK ("mcp_admission"."state" IN ('queued','admitted')) +); +--> statement-breakpoint +CREATE TABLE "mcp_admission_scope" ( + "id" text PRIMARY KEY NOT NULL +); +--> statement-breakpoint +CREATE TABLE "mcp_approval" ( + "execution_id" text PRIMARY KEY NOT NULL, + "deployment_id" text NOT NULL, + "session_id" text NOT NULL, + "request_digest" text NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "decision" text NOT NULL, + "actor_id" text, + "decided_at" timestamp with time zone, + "consumed_at" timestamp with time zone, + CONSTRAINT "mcp_approval_decision_check" CHECK ("mcp_approval"."decision" IN ('pending','approved','denied','expired')), + CONSTRAINT "mcp_approval_consumed_check" CHECK ("mcp_approval"."consumed_at" IS NULL OR ("mcp_approval"."decision" = 'approved' AND "mcp_approval"."actor_id" IS NOT NULL)) +); +--> statement-breakpoint +CREATE TABLE "mcp_catalog" ( + "id" text PRIMARY KEY NOT NULL, + "connection_id" text NOT NULL, + "kind" text NOT NULL, + "revision" integer NOT NULL, + "config_revision" integer NOT NULL, + "auth_generation" integer NOT NULL, + "credential_identity" text NOT NULL, + "tools" jsonb NOT NULL, + "fingerprint" text NOT NULL, + "actor_id" text NOT NULL, + "created_at" timestamp with time zone NOT NULL, + CONSTRAINT "mcp_catalog_kind_check" CHECK ("mcp_catalog"."kind" IN ('draft','published') AND (("mcp_catalog"."kind" = 'draft' AND "mcp_catalog"."revision" = 0) OR ("mcp_catalog"."kind" = 'published' AND "mcp_catalog"."revision" > 0))) +); +--> statement-breakpoint +CREATE TABLE "mcp_execution" ( + "id" text PRIMARY KEY NOT NULL, + "task_id" text NOT NULL, + "session_id" text NOT NULL, + "run_id" text NOT NULL, + "tool_call_id" text NOT NULL, + "connection_id" text NOT NULL, + "catalog_id" text NOT NULL, + "principal_id" text NOT NULL, + "binding_hash" text NOT NULL, + "request_digest" text NOT NULL, + "argument_hash" text NOT NULL, + "upstream_name" text NOT NULL, + "config_revision" integer NOT NULL, + "auth_generation" integer NOT NULL, + "classification" text NOT NULL, + "state" text NOT NULL, + "version" integer DEFAULT 1 NOT NULL, + "boot_id" text, + "intent_id" text, + "intent_expires_at" timestamp with time zone, + "deadline" timestamp with time zone NOT NULL, + "result" jsonb, + "result_unavailable" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + CONSTRAINT "mcp_execution_state_check" CHECK ("mcp_execution"."state" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')), + CONSTRAINT "mcp_execution_classification_check" CHECK ("mcp_execution"."classification" IN ('read','write')), + CONSTRAINT "mcp_execution_dispatch_check" CHECK ("mcp_execution"."state" NOT IN ('dispatching','outcome_unknown','succeeded') OR ("mcp_execution"."boot_id" IS NOT NULL AND "mcp_execution"."intent_id" IS NOT NULL AND "mcp_execution"."intent_expires_at" IS NOT NULL)) +); +--> statement-breakpoint +CREATE TABLE "mcp_resolution" ( + "execution_id" text PRIMARY KEY NOT NULL, + "actor_id" text NOT NULL, + "outcome" text NOT NULL, + "created_at" timestamp with time zone NOT NULL, + CONSTRAINT "mcp_resolution_outcome_check" CHECK ("mcp_resolution"."outcome" IN ('succeeded','failed','acknowledged')) +); +--> statement-breakpoint +ALTER TABLE "integration_connection" ADD COLUMN "config_revision" integer DEFAULT 1 NOT NULL;--> statement-breakpoint +ALTER TABLE "integration_connection" ADD COLUMN "auth_generation" integer DEFAULT 1 NOT NULL;--> statement-breakpoint +ALTER TABLE "integration_connection" ADD COLUMN "credential_identity" text DEFAULT '' NOT NULL;--> statement-breakpoint +ALTER TABLE "integration_connection" ADD COLUMN "catalog_revision" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "integration_connection" ADD COLUMN "revoked" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD CONSTRAINT "mcp_admission_execution_id_mcp_execution_id_fk" FOREIGN KEY ("execution_id") REFERENCES "public"."mcp_execution"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD CONSTRAINT "mcp_admission_deployment_id_mcp_admission_scope_id_fk" FOREIGN KEY ("deployment_id") REFERENCES "public"."mcp_admission_scope"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD CONSTRAINT "mcp_admission_connection_id_integration_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."integration_connection"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_approval" ADD CONSTRAINT "mcp_approval_execution_id_mcp_execution_id_fk" FOREIGN KEY ("execution_id") REFERENCES "public"."mcp_execution"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_catalog" ADD CONSTRAINT "mcp_catalog_connection_id_integration_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."integration_connection"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_execution" ADD CONSTRAINT "mcp_execution_connection_id_integration_connection_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."integration_connection"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_execution" ADD CONSTRAINT "mcp_execution_catalog_id_mcp_catalog_id_fk" FOREIGN KEY ("catalog_id") REFERENCES "public"."mcp_catalog"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_execution" ADD CONSTRAINT "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk" FOREIGN KEY ("task_id","session_id") REFERENCES "public"."task_session"("task_id","session_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_resolution" ADD CONSTRAINT "mcp_resolution_execution_id_mcp_execution_id_fk" FOREIGN KEY ("execution_id") REFERENCES "public"."mcp_execution"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "mcp_admission_deployment_idx" ON "mcp_admission" USING btree ("deployment_id","state","expires_at");--> statement-breakpoint +CREATE INDEX "mcp_admission_connection_idx" ON "mcp_admission" USING btree ("connection_id","state");--> statement-breakpoint +CREATE INDEX "mcp_admission_session_idx" ON "mcp_admission" USING btree ("session_id","state");--> statement-breakpoint +CREATE INDEX "mcp_approval_pending_idx" ON "mcp_approval" USING btree ("deployment_id","decision","expires_at");--> statement-breakpoint +CREATE INDEX "mcp_approval_session_idx" ON "mcp_approval" USING btree ("session_id","decision");--> statement-breakpoint +CREATE INDEX "mcp_catalog_connection_idx" ON "mcp_catalog" USING btree ("connection_id","created_at");--> statement-breakpoint +CREATE UNIQUE INDEX "mcp_catalog_published_revision_idx" ON "mcp_catalog" USING btree ("connection_id","revision") WHERE kind = 'published';--> statement-breakpoint +CREATE INDEX "mcp_execution_task_session_idx" ON "mcp_execution" USING btree ("task_id","session_id");--> statement-breakpoint +CREATE UNIQUE INDEX "mcp_execution_call_idx" ON "mcp_execution" USING btree ("session_id","tool_call_id");--> statement-breakpoint +CREATE INDEX "mcp_execution_connection_state_idx" ON "mcp_execution" USING btree ("connection_id","session_id","state");--> statement-breakpoint +CREATE INDEX "mcp_execution_catalog_idx" ON "mcp_execution" USING btree ("catalog_id");--> statement-breakpoint +CREATE INDEX "mcp_execution_pending_idx" ON "mcp_execution" USING btree ("state","deadline");--> statement-breakpoint +CREATE UNIQUE INDEX "mcp_execution_intent_idx" ON "mcp_execution" USING btree ("intent_id");--> statement-breakpoint +CREATE FUNCTION mcp_catalog_immutable() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + RAISE EXCEPTION 'MCP catalogs are immutable'; +END; +$$; +--> statement-breakpoint +CREATE TRIGGER mcp_catalog_immutable_update BEFORE UPDATE ON mcp_catalog FOR EACH ROW EXECUTE FUNCTION mcp_catalog_immutable(); +--> statement-breakpoint +CREATE FUNCTION mcp_execution_transition() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF (to_jsonb(NEW) - ARRAY['state','version','boot_id','intent_id','intent_expires_at','result','result_unavailable','updated_at']) IS DISTINCT FROM + (to_jsonb(OLD) - ARRAY['state','version','boot_id','intent_id','intent_expires_at','result','result_unavailable','updated_at']) THEN + RAISE EXCEPTION 'MCP request identity is immutable'; + END IF; + IF NOT ((OLD.state = 'waiting_approval' AND NEW.state IN ('authorized','canceled')) OR + (OLD.state = 'authorized' AND NEW.state IN ('dispatching','canceled')) OR + (OLD.state = 'dispatching' AND NEW.state IN ('succeeded','failed','outcome_unknown')) OR + (OLD.state = 'outcome_unknown' AND NEW.state IN ('succeeded','failed'))) OR NEW.version <> OLD.version + 1 THEN + RAISE EXCEPTION 'Invalid MCP execution transition'; + END IF; + IF OLD.intent_id IS NOT NULL AND (NEW.intent_id, NEW.boot_id, NEW.intent_expires_at) IS DISTINCT FROM (OLD.intent_id, OLD.boot_id, OLD.intent_expires_at) THEN + RAISE EXCEPTION 'MCP dispatch intent is immutable'; + END IF; + RETURN NEW; +END; +$$; +--> statement-breakpoint +CREATE TRIGGER mcp_execution_transition_update BEFORE UPDATE ON mcp_execution FOR EACH ROW EXECUTE FUNCTION mcp_execution_transition(); diff --git a/orchestrator/drizzle/0102_mcp_authorization_kind.sql b/orchestrator/drizzle/0102_mcp_authorization_kind.sql new file mode 100644 index 000000000..41d4d8be9 --- /dev/null +++ b/orchestrator/drizzle/0102_mcp_authorization_kind.sql @@ -0,0 +1,3 @@ +ALTER TABLE "mcp_execution" ADD COLUMN "authorization_kind" text NOT NULL;--> statement-breakpoint +ALTER TABLE "mcp_execution" ADD COLUMN "automation_grant_hash" text;--> statement-breakpoint +ALTER TABLE "mcp_execution" ADD CONSTRAINT "mcp_execution_authorization_check" CHECK (("mcp_execution"."authorization_kind" = 'read' AND "mcp_execution"."classification" = 'read' AND "mcp_execution"."automation_grant_hash" IS NULL) OR ("mcp_execution"."authorization_kind" = 'approval' AND "mcp_execution"."automation_grant_hash" IS NULL) OR ("mcp_execution"."authorization_kind" = 'automation' AND "mcp_execution"."classification" = 'write' AND "mcp_execution"."automation_grant_hash" IS NOT NULL)); \ No newline at end of file diff --git a/orchestrator/drizzle/0103_mcp_session_retention.sql b/orchestrator/drizzle/0103_mcp_session_retention.sql new file mode 100644 index 000000000..1807c1bf0 --- /dev/null +++ b/orchestrator/drizzle/0103_mcp_session_retention.sql @@ -0,0 +1,12 @@ +ALTER TABLE "mcp_admission" DROP CONSTRAINT "mcp_admission_execution_id_mcp_execution_id_fk"; +--> statement-breakpoint +ALTER TABLE "mcp_approval" DROP CONSTRAINT "mcp_approval_execution_id_mcp_execution_id_fk"; +--> statement-breakpoint +ALTER TABLE "mcp_execution" DROP CONSTRAINT "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk"; +--> statement-breakpoint +ALTER TABLE "mcp_resolution" DROP CONSTRAINT "mcp_resolution_execution_id_mcp_execution_id_fk"; +--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD CONSTRAINT "mcp_admission_execution_id_mcp_execution_id_fk" FOREIGN KEY ("execution_id") REFERENCES "public"."mcp_execution"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_approval" ADD CONSTRAINT "mcp_approval_execution_id_mcp_execution_id_fk" FOREIGN KEY ("execution_id") REFERENCES "public"."mcp_execution"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_execution" ADD CONSTRAINT "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk" FOREIGN KEY ("task_id","session_id") REFERENCES "public"."task_session"("task_id","session_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mcp_resolution" ADD CONSTRAINT "mcp_resolution_execution_id_mcp_execution_id_fk" FOREIGN KEY ("execution_id") REFERENCES "public"."mcp_execution"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/orchestrator/drizzle/meta/0101_snapshot.json b/orchestrator/drizzle/meta/0101_snapshot.json new file mode 100644 index 000000000..ded836949 --- /dev/null +++ b/orchestrator/drizzle/meta/0101_snapshot.json @@ -0,0 +1,8410 @@ +{ + "id": "6c26085f-c08f-4ad6-8f03-e166886de7da", + "prevId": "785b7a9a-c956-4d05-81af-fdf4b2849339", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/0102_snapshot.json b/orchestrator/drizzle/meta/0102_snapshot.json new file mode 100644 index 000000000..a81a7fab5 --- /dev/null +++ b/orchestrator/drizzle/meta/0102_snapshot.json @@ -0,0 +1,8426 @@ +{ + "id": "d8ddf3d2-6bdb-43cd-820b-326c21ce29c1", + "prevId": "6c26085f-c08f-4ad6-8f03-e166886de7da", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/0103_snapshot.json b/orchestrator/drizzle/meta/0103_snapshot.json new file mode 100644 index 000000000..02ed1641c --- /dev/null +++ b/orchestrator/drizzle/meta/0103_snapshot.json @@ -0,0 +1,8426 @@ +{ + "id": "47c316d6-8319-40d1-b585-82c04b895dc9", + "prevId": "d8ddf3d2-6bdb-43cd-820b-326c21ce29c1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/_journal.json b/orchestrator/drizzle/meta/_journal.json index 7288b7157..6f9af3815 100644 --- a/orchestrator/drizzle/meta/_journal.json +++ b/orchestrator/drizzle/meta/_journal.json @@ -708,6 +708,27 @@ "when": 1793241391817, "tag": "0100_profile_programmatic_only", "breakpoints": true + }, + { + "idx": 101, + "version": "7", + "when": 1793585018303, + "tag": "0101_mcp_authority", + "breakpoints": true + }, + { + "idx": 102, + "version": "7", + "when": 1793585413259, + "tag": "0102_mcp_authorization_kind", + "breakpoints": true + }, + { + "idx": 103, + "version": "7", + "when": 1793585592275, + "tag": "0103_mcp_session_retention", + "breakpoints": true } ] } \ No newline at end of file diff --git a/orchestrator/src/__tests__/mcp-domain.test.ts b/orchestrator/src/__tests__/mcp-domain.test.ts new file mode 100644 index 000000000..cfafc10d2 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-domain.test.ts @@ -0,0 +1,165 @@ +import { describe, expect, test } from "bun:test"; +import { validateMcpConfig } from "../mcp/config.ts"; +import { reviewCatalog } from "../mcp/catalog.ts"; +import { canonicalDigest } from "../mcp/hash.ts"; +import { compileBindings, assertBindingCurrent } from "../mcp/bindings.ts"; +import { validateMcpGrant } from "../mcp/types.ts"; + +const config = { + endpoint: "https://tools.example.test/mcp", + zone: "public", + auth: { kind: "api_key" as const, secretRef: "mcp.service.api-key" }, +}; +const tool = { + upstreamName: "Search.Documents", + description: "Search documents", + inputSchema: { type: "object" }, + classification: "read" as const, + enabled: true, +}; +const state = { + configRevision: 1, + authGeneration: 1, + credentialIdentity: "account-a", + catalogRevision: 1, + revoked: false, +}; +const grant = { + connectionId: "c1", + operation: "invoke", + credentialScope: "org" as const, + resourceConstraints: [], + mcp: { + upstreamName: tool.upstreamName, + automationWrite: false, + argumentConstraints: {}, + unrestricted: true, + }, +}; + +describe("MCP domain", () => { + test("accepts canonical endpoint and references only", () => { + expect(validateMcpConfig(config)).toEqual(config); + expect( + validateMcpConfig({ ...config, endpoint: "https://tools/mcp", zone: "internal" }).zone, + ).toBe("internal"); + expect(() => + validateMcpConfig({ ...config, endpoint: "https://bad..example.test/mcp" }), + ).toThrow(); + for (const endpoint of [ + "http://tools.example.test/mcp", + "https://127.0.0.1/mcp", + "https://[::1]/mcp", + "https://u:p@tools.example.test/mcp", + "https://tools.example.test/mcp?q=1", + "https://tools.example.test/mcp#x", + "https://tools.example.test:443/mcp", + ]) { + expect(() => validateMcpConfig({ ...config, endpoint })).toThrow(); + } + expect(() => validateMcpConfig({ ...config, token: "secret" })).toThrow(); + expect(() => + validateMcpConfig({ + ...config, + auth: { kind: "api_key" as const, secretRef: "unrelated.key" }, + }), + ).toThrow(); + }); + test("requires explicit tool scope and unrestricted selection", () => { + expect(() => validateMcpGrant(grant)).not.toThrow(); + expect(() => validateMcpGrant({ ...grant, credentialScope: "user" })).toThrow(); + expect(() => + validateMcpGrant({ ...grant, mcp: { ...grant.mcp, unrestricted: false } }), + ).toThrow(); + expect(() => validateMcpGrant({ ...grant, operation: tool.upstreamName })).toThrow(); + }); + test("catalog fingerprints bind upstream schema and explicit review", () => { + const reviewed = reviewCatalog([tool]); + expect(reviewed[0]!.fingerprint).toBe(canonicalDigest(tool)); + expect(() => reviewCatalog([{ ...tool, classification: undefined }])).toThrow(); + expect(() => reviewCatalog([tool, tool])).toThrow(); + expect(() => + reviewCatalog([{ ...tool, inputSchema: { $ref: "https://example.test/schema" } }]), + ).toThrow(); + }); + test("freezes names and authority and rejects live changes", () => { + const bindings = compileBindings([ + { + id: "c1", + alias: "docs", + config, + state, + enabled: true, + tools: reviewCatalog([tool]), + grants: [grant], + }, + ]); + expect(bindings[0]!.tools[0]!.name).toMatch(/^mcp_docs_/); + expect(() => assertBindingCurrent(bindings[0]!, { ...state, enabled: true })).not.toThrow(); + expect(() => + assertBindingCurrent(bindings[0]!, { ...state, enabled: true, authGeneration: 2 }), + ).toThrow(); + expect(() => assertBindingCurrent(bindings[0]!, { ...state, enabled: false })).toThrow(); + const before = bindings[0]!.hash; + config.auth.secretRef = "mcp.service.replacement"; + expect(bindings[0]!.config.auth).toEqual({ + kind: "api_key" as const, + secretRef: "mcp.service.api-key", + }); + expect(bindings[0]!.hash).toBe(before); + config.auth.secretRef = "mcp.service.api-key"; + }); + test("canonical hash rejects unsupported and non-finite JSON", () => { + expect(canonicalDigest({ b: 1, a: 2 })).toBe(canonicalDigest({ a: 2, b: 1 })); + expect(() => canonicalDigest({ n: NaN })).toThrow(); + expect(() => canonicalDigest({ n: undefined })).toThrow(); + }); +}); + +test("MCP child grants intersect constraints and automation permission", async () => { + const { intersectMcpGrants } = await import("../mcp/bindings.ts"); + const parent = { + ...grant, + mcp: { + ...grant.mcp, + unrestricted: false, + argumentConstraints: { properties: { project: { const: "allowed" } }, required: ["project"] }, + }, + }; + const child = { ...grant, mcp: { ...grant.mcp, automationWrite: true } }; + const [effective] = intersectMcpGrants([parent], [child]); + expect(effective!.mcp!.automationWrite).toBe(false); + expect(effective!.mcp!.argumentConstraints).toEqual(parent.mcp.argumentConstraints); + expect(() => intersectMcpGrants([], [child])).toThrow(); +}); + +test("MCP grants cannot become generic guest capabilities", async () => { + const { grantsToCapabilities } = await import("../integrations/grants.ts"); + const connection = { + id: "c1", + alias: "docs", + provider: "mcp", + displayName: "Docs", + config, + enabled: true, + isDefault: false, + testedAt: null, + createdAt: new Date(0), + updatedAt: new Date(0), + }; + expect(grantsToCapabilities([{ grant, connection }])).toEqual([]); +}); + +test("schema keyword checks do not reinterpret ordinary property names or constants", async () => { + const { assertSchemaShape } = await import("../mcp/catalog.ts"); + expect(() => + assertSchemaShape({ + type: "object", + properties: { $ref: { type: "string" } }, + const: { $schema: "ordinary data" }, + }), + ).not.toThrow(); + expect(() => + assertSchemaShape({ properties: { child: { $ref: "https://example.test/schema" } } }), + ).toThrow("local"); +}); diff --git a/orchestrator/src/__tests__/mcp-rpc.test.ts b/orchestrator/src/__tests__/mcp-rpc.test.ts new file mode 100644 index 000000000..719797368 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-rpc.test.ts @@ -0,0 +1,212 @@ +import { describe, expect, test } from "bun:test"; +import { createClient, Code, ConnectError } from "@connectrpc/connect"; +import { createConnectTransport } from "@connectrpc/connect-node"; +import { Hono } from "hono"; +import type { AddressInfo } from "node:net"; +import { buildServer } from "../server.ts"; +import { IntegrationService } from "../gen/engram/app/v1/integration_pb.ts"; +import { mcpHandlers, type McpServices } from "../mcp/contracts.ts"; +import type { + IntegrationConnectionRow, + IntegrationConnectionStore, +} from "../db/integration-connections.ts"; +import { reviewCatalog } from "../mcp/catalog.ts"; + +const unavailable = async (): Promise => { + throw new Error("unexpected service invocation"); +}; +const row: IntegrationConnectionRow = { + id: "connection-1", + alias: "tools", + provider: "mcp", + displayName: "Tools", + isDefault: false, + enabled: false, + testedAt: null, + createdAt: new Date(0), + updatedAt: new Date(0), + config: { + endpoint: "https://tools.example.test/mcp", + zone: "public", + auth: { kind: "api_key", secretRef: "mcp.tools.api-key" }, + }, + mcp: { + configRevision: 1, + authGeneration: 1, + credentialIdentity: "account-a", + catalogRevision: 0, + revoked: false, + }, +}; +const connections: IntegrationConnectionStore = { + list: async () => [row], + get: async (id) => (id === row.id ? row : null), + getMany: async () => [row], + getDefault: async () => null, + create: unavailable, + update: unavailable, + delete: unavailable, + markTested: unavailable, + setEnabled: unavailable, + setConfig: unavailable, + ensureDefault: unavailable, +}; +function services(): McpServices { + return { + enabled: true, + repository: { + updateConfig: unavailable, + setAuthorization: unavailable, + disconnect: unavailable, + setEnabled: unavailable, + saveDraft: unavailable, + publish: unavailable, + getCatalog: unavailable, + listCatalogs: unavailable, + }, + journal: { + get: unavailable, + hasUnknown: unavailable, + create: unavailable, + requestApproval: unavailable, + pending: unavailable, + decide: unavailable, + admit: unavailable, + dispatch: unavailable, + finish: unavailable, + cancel: unavailable, + resolveUnknown: unavailable, + expire: unavailable, + }, + }; +} +async function spawn(service?: McpServices, role = "admin") { + const server = buildServer(new Hono(), (router) => + router.service( + IntegrationService, + mcpHandlers({ + services: service, + connections, + getSession: async () => ({ user: { id: "owner", role } }), + now: () => new Date(0), + connectionToProto: (c) => ({ id: c.id }), + }), + ), + ); + const url = await new Promise((resolve) => + server.listen(0, "127.0.0.1", () => + resolve(`http://127.0.0.1:${(server.address() as AddressInfo).port}/rpc`), + ), + ); + return { + client: createClient( + IntegrationService, + createConnectTransport({ baseUrl: url, httpVersion: "1.1" }), + ), + close: () => + new Promise((resolve, reject) => server.close((e) => (e ? reject(e) : resolve()))), + }; +} +async function code(promise: Promise, expected: Code) { + try { + await promise; + throw new Error("request should fail"); + } catch (e) { + expect(e).toBeInstanceOf(ConnectError); + expect((e as ConnectError).code).toBe(expected); + } +} +describe("MCP native wire contract", () => { + test("all new methods fail closed by default", async () => { + const s = await spawn(); + try { + await code( + s.client.discoverMcp({ connectionId: row.id, expectedRevision: 1 }), + Code.FailedPrecondition, + ); + await code(s.client.publishMcp({}), Code.FailedPrecondition); + await code(s.client.authorizeMcp({}), Code.FailedPrecondition); + await code(s.client.disconnectMcp({}), Code.FailedPrecondition); + await code(s.client.listMcpApprovals({}), Code.FailedPrecondition); + await code(s.client.decideMcpApproval({}), Code.FailedPrecondition); + await code(s.client.cancelMcpCall({}), Code.FailedPrecondition); + await code(s.client.resolveMcpOutcome({}), Code.FailedPrecondition); + await code(s.client.rebindMcp({}), Code.FailedPrecondition); + } finally { + await s.close(); + } + }); + test("admin controls and missing transport fail before services run", async () => { + const member = await spawn(services(), "user"); + const admin = await spawn(services()); + try { + await code(member.client.discoverMcp({ connectionId: row.id }), Code.PermissionDenied); + await code(member.client.publishMcp({}), Code.PermissionDenied); + await code(member.client.authorizeMcp({}), Code.PermissionDenied); + await code(admin.client.discoverMcp({ connectionId: row.id }), Code.FailedPrecondition); + await code( + admin.client.rebindMcp({ sessionId: "s1", expectedBindingHash: "hash" }), + Code.FailedPrecondition, + ); + } finally { + await member.close(); + await admin.close(); + } + }); + test("publication preserves schema and classification and maps stale CAS", async () => { + const service = services(); + let calls = 0; + service.validateSchemas = async (tools) => { + expect(tools[0]!.classification).toBe("write"); + }; + service.repository.publish = async (_connectionId, _draftId, expected, input) => { + calls++; + if (expected !== 0) throw new Error("stale revision"); + const tools = reviewCatalog(input); + return { + id: "catalog-1", + connectionId: row.id, + kind: "published", + revision: 1, + configRevision: 1, + authGeneration: 1, + credentialIdentity: "account-a", + tools, + fingerprint: "catalog-hash", + actorId: "owner", + createdAt: new Date(0), + }; + }; + const s = await spawn(service); + const request = { + connectionId: row.id, + draftId: "draft-1", + expectedRevision: 0, + tools: [ + { + upstreamName: "Create.Record", + description: "Create a record", + inputSchemaJson: '{"type":"object","additionalProperties":false}', + classification: "write", + enabled: true, + }, + ], + }; + try { + const response = await s.client.publishMcp(request); + expect(response.catalog?.tools[0]?.classification).toBe("write"); + expect(response.catalog?.tools[0]?.inputSchemaJson).toBe(request.tools[0]!.inputSchemaJson); + await code(s.client.publishMcp({ ...request, expectedRevision: 1 }), Code.Aborted); + await code( + s.client.publishMcp({ + ...request, + tools: [{ ...request.tools[0]!, inputSchemaJson: "not json" }], + }), + Code.InvalidArgument, + ); + expect(calls).toBe(2); + } finally { + await s.close(); + } + }); +}); diff --git a/orchestrator/src/__tests__/mcp-store.test.ts b/orchestrator/src/__tests__/mcp-store.test.ts new file mode 100644 index 000000000..2146cd8f2 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-store.test.ts @@ -0,0 +1,517 @@ +import { describe, expect, test } from "bun:test"; +import { eq, inArray, sql } from "drizzle-orm"; +import { checkDb, getDb } from "../db/client.ts"; +import { makeIntegrationConnectionStore } from "../db/integration-connections.ts"; +import { + integrationConnection, + mcpAdmission, + mcpAdmissionScope, + mcpApproval, + mcpCatalog, + mcpExecution, + mcpResolution, + task, + taskSession, +} from "../db/schema.ts"; +import { makeMcpRepository } from "../mcp/repository.ts"; +import { makeMcpJournal } from "../mcp/journal.ts"; +import { canonicalDigest } from "../mcp/hash.ts"; + +const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; +const tool = { + upstreamName: "Write.Record", + description: "Write a record", + inputSchema: { type: "object" }, +}; +const config = { + endpoint: "https://tools.example.test/mcp", + zone: "public", + auth: { kind: "api_key", secretRef: "mcp.service.api-key" }, +}; +async function fixture() { + const db = getDb(); + const connections = makeIntegrationConnectionStore(db); + const row = await connections.create({ + alias: `mcp-${crypto.randomUUID()}`, + provider: "mcp", + displayName: "Tools", + config, + }); + const taskId = crypto.randomUUID(); + const sessionId = crypto.randomUUID(); + await db.insert(task).values({ id: taskId, type: "chat", createdByUserId: "owner" }); + await db.insert(taskSession).values({ taskId, sessionId }); + const repo = makeMcpRepository(db); + const now = new Date("2026-01-01T00:00:00Z"); + await repo.setAuthorization(row.id, 1, 1, "account-a", now); + const live = (await connections.get(row.id))!; + const draft = await repo.saveDraft(row.id, live.mcp!, [tool], "admin", now); + const published = await repo.publish( + row.id, + draft.id, + 0, + [{ ...tool, classification: "write", enabled: true }], + "admin", + now, + ); + await repo.setEnabled(row.id, 1, true, now); + const journal = makeMcpJournal(db); + const deploymentId = crypto.randomUUID(); + const input = { + taskId, + sessionId, + runId: "run-1", + toolCallId: "call-1", + connectionId: row.id, + catalogId: published.id, + principalId: "owner", + bindingHash: canonicalDigest({ binding: 1 }), + argumentHash: canonicalDigest({ x: 1 }), + upstreamName: tool.upstreamName, + configRevision: 1, + authGeneration: live.mcp!.authGeneration, + classification: "write" as const, + deadline: new Date(now.getTime() + 60_000), + }; + return { + db, + connections, + row, + repo, + now, + draft, + published, + journal, + input, + deploymentId, + async cleanup() { + const executions = await db + .select({ id: mcpExecution.id }) + .from(mcpExecution) + .where(eq(mcpExecution.sessionId, sessionId)); + const ids = executions.map((e) => e.id); + if (ids.length) { + await db.delete(mcpAdmission).where(inArray(mcpAdmission.executionId, ids)); + await db.delete(mcpApproval).where(inArray(mcpApproval.executionId, ids)); + await db.delete(mcpResolution).where(inArray(mcpResolution.executionId, ids)); + await db.delete(mcpExecution).where(inArray(mcpExecution.id, ids)); + } + await db.delete(mcpAdmissionScope).where(eq(mcpAdmissionScope.id, deploymentId)); + await db.delete(mcpCatalog).where(eq(mcpCatalog.connectionId, row.id)); + await db.delete(task).where(eq(task.id, taskId)); + await connections.delete(row.id); + }, + }; +} + +describe("MCP durable authority", () => { + test.skipIf(!reachable)("publish CAS has one winner and refuses a stale discovery", async () => { + const f = await fixture(); + try { + const outcomes = await Promise.allSettled( + [1, 2].map(() => + f.repo.publish( + f.row.id, + f.draft.id, + 1, + [{ ...tool, classification: "write", enabled: true }], + "admin", + f.now, + ), + ), + ); + expect(outcomes.filter((o) => o.status === "fulfilled")).toHaveLength(1); + await f.repo.updateConfig( + f.row.id, + 1, + { alias: f.row.alias, displayName: "Tools", config }, + f.now, + ); + await expect( + f.repo.publish( + f.row.id, + f.draft.id, + 2, + [{ ...tool, classification: "write", enabled: true }], + "admin", + f.now, + ), + ).rejects.toThrow("stale"); + expect(await f.connections.markTested(f.row.id, f.now)).toBeNull(); + } finally { + await f.cleanup(); + } + }); + test.skipIf(!reachable)( + "call digest, approval and dispatch are single-use under concurrency", + async () => { + const f = await fixture(); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + expect((await f.journal.create(f.input, { kind: "approval" }, f.now)).id).toBe(call.id); + await expect( + f.journal.create( + { ...f.input, argumentHash: canonicalDigest({ x: 2 }) }, + { kind: "approval" }, + f.now, + ), + ).rejects.toThrow("digest"); + await f.journal.requestApproval(call.id, f.deploymentId, f.now); + await expect(f.journal.decide(call.id, "intruder", false, true, f.now)).rejects.toThrow( + "owner", + ); + await f.journal.decide(call.id, "owner", false, true, f.now); + await f.journal.admit(call.id, f.deploymentId, f.now); + const results = await Promise.allSettled( + [1, 2].map((n) => + f.journal.dispatch( + call.id, + { + bootId: "boot-1", + intentId: `intent-${call.id}-${n}`, + expiresAt: new Date(f.now.getTime() + 10_000), + }, + f.now, + ), + ), + ); + expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(1); + const current = (await f.journal.get(call.id))!; + expect(current.state).toBe("dispatching"); + await f.journal.finish(call.id, current.version, "outcome_unknown", null, false, f.now); + const next = await f.journal.create( + { ...f.input, toolCallId: "call-2" }, + { + kind: "automation", + grant: { + upstreamName: tool.upstreamName, + automationWrite: true, + argumentConstraints: {}, + unrestricted: true, + }, + }, + f.now, + ); + await f.journal.admit(next.id, f.deploymentId, f.now); + await expect( + f.journal.dispatch( + next.id, + { + bootId: "boot-1", + intentId: `next-${call.id}`, + expiresAt: new Date(f.now.getTime() + 10_000), + }, + f.now, + ), + ).rejects.toThrow("unknown"); + await f.journal.resolveUnknown(call.id, "owner", false, "acknowledged", f.now); + expect(await f.journal.hasUnknown(f.input.sessionId, f.row.id)).toBe(false); + await expect( + Promise.resolve( + f.db.execute( + sql`UPDATE mcp_execution SET request_digest = 'changed' WHERE id = ${call.id}`, + ), + ), + ).rejects.toThrow(); + } finally { + await f.cleanup(); + } + }, + ); + test.skipIf(!reachable)( + "distributed admission bounds session concurrency and expires pending approvals", + async () => { + const f = await fixture(); + try { + const calls = await Promise.all( + Array.from({ length: 6 }, (_, n) => + f.journal.create( + { ...f.input, toolCallId: `call-${n}`, classification: "read" }, + { kind: "read" }, + f.now, + ), + ), + ); + const statuses = await Promise.all( + calls.map((c) => f.journal.admit(c.id, f.deploymentId, f.now)), + ); + expect(statuses.filter((s) => s === "admitted")).toHaveLength(4); + expect(statuses.filter((s) => s === "queued")).toHaveLength(2); + const pending = await f.journal.create( + { ...f.input, toolCallId: "approval" }, + { kind: "approval" }, + f.now, + ); + await f.journal.requestApproval(pending.id, f.deploymentId, f.now); + await f.journal.expire(f.deploymentId, new Date(f.now.getTime() + 601_000)); + expect((await f.journal.get(pending.id))!.state).toBe("canceled"); + expect(await f.journal.pending(f.input.sessionId, "owner", false, f.now)).toHaveLength(0); + } finally { + await f.cleanup(); + } + }, + ); +}); + +describe("MCP authority isolation", () => { + test.skipIf(!reachable)( + "rejects cross-connection catalog and unapproved write transitions", + async () => { + const first = await fixture(); + const second = await fixture(); + try { + await expect( + first.journal.create(first.input, { kind: "read" }, first.now), + ).rejects.toThrow("write requires"); + await expect( + first.journal.create( + first.input, + { + kind: "automation", + grant: { + upstreamName: tool.upstreamName, + automationWrite: false, + argumentConstraints: {}, + unrestricted: true, + }, + }, + first.now, + ), + ).rejects.toThrow("explicit automation"); + const call = await first.journal.create( + { ...first.input, catalogId: second.published.id }, + { kind: "approval" }, + first.now, + ); + await first.journal.requestApproval(call.id, first.deploymentId, first.now); + await first.journal.decide(call.id, "owner", false, true, first.now); + await first.journal.admit(call.id, first.deploymentId, first.now); + await expect( + first.journal.dispatch( + call.id, + { + bootId: "boot", + intentId: crypto.randomUUID(), + expiresAt: new Date(first.now.getTime() + 1000), + }, + first.now, + ), + ).rejects.toThrow("stale"); + await expect( + Promise.resolve( + first.db.execute( + sql`UPDATE mcp_execution SET state = 'succeeded', version = version + 1 WHERE id = ${call.id}`, + ), + ), + ).rejects.toThrow(); + await expect( + Promise.resolve( + first.db.execute( + sql`UPDATE mcp_catalog SET tools = '[]'::jsonb WHERE id = ${first.published.id}`, + ), + ), + ).rejects.toThrow(); + await expect( + first.journal.pending(first.input.sessionId, "intruder", false, first.now), + ).resolves.toHaveLength(0); + } finally { + await first.cleanup(); + await second.cleanup(); + } + }, + ); + test.skipIf(!reachable)( + "pending approvals are owner-scoped and expired approvals cannot dispatch", + async () => { + const f = await fixture(); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await f.journal.requestApproval(call.id, f.deploymentId, f.now); + await expect( + f.journal.pending(f.input.sessionId, "intruder", false, f.now), + ).rejects.toThrow("owner"); + expect(await f.journal.pending(f.input.sessionId, "owner", false, f.now)).toHaveLength(1); + await f.journal.decide(call.id, "owner", false, true, f.now); + await f.journal.admit(call.id, f.deploymentId, f.now); + const later = new Date(f.now.getTime() + 61_000); + await expect( + f.journal.dispatch( + call.id, + { + bootId: "boot", + intentId: crypto.randomUUID(), + expiresAt: new Date(later.getTime() + 1000), + }, + later, + ), + ).rejects.toThrow("cannot dispatch"); + await f.journal.expire(f.deploymentId, later); + expect((await f.journal.get(call.id))!.state).toBe("canceled"); + } finally { + await f.cleanup(); + } + }, + ); + test.skipIf(!reachable)( + "an expired dispatch becomes unknown and accepts one verified late result", + async () => { + const f = await fixture(); + try { + const call = await f.journal.create( + f.input, + { + kind: "automation", + grant: { + upstreamName: tool.upstreamName, + automationWrite: true, + argumentConstraints: {}, + unrestricted: true, + }, + }, + f.now, + ); + await f.journal.admit(call.id, f.deploymentId, f.now); + await f.journal.dispatch( + call.id, + { + bootId: "boot", + intentId: crypto.randomUUID(), + expiresAt: new Date(f.now.getTime() + 1000), + }, + f.now, + ); + await f.journal.expire(f.deploymentId, new Date(f.now.getTime() + 61_000)); + const unknown = (await f.journal.get(call.id))!; + expect(unknown.state).toBe("outcome_unknown"); + expect(await f.journal.hasUnknown(f.input.sessionId, f.row.id)).toBe(true); + const late = await f.journal.finish( + call.id, + unknown.version, + "succeeded", + null, + true, + new Date(f.now.getTime() + 62_000), + ); + expect(late.resultUnavailable).toBe(true); + expect(await f.journal.hasUnknown(f.input.sessionId, f.row.id)).toBe(false); + await expect( + f.journal.finish(call.id, unknown.version, "failed", null, false, f.now), + ).rejects.toThrow("stale"); + } finally { + await f.cleanup(); + } + }, + ); +}); + +test.skipIf(!reachable)( + "approval has a ten-minute window separate from the tool deadline", + async () => { + const f = await fixture(); + try { + const call = await f.journal.create( + { ...f.input, deadline: new Date(f.now.getTime() + 690_000) }, + { kind: "approval" }, + f.now, + ); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, f.now); + expect(approval.expiresAt.getTime() - f.now.getTime()).toBe(600_000); + const afterFiveMinutes = new Date(f.now.getTime() + 300_000); + await f.journal.decide(call.id, "owner", false, true, afterFiveMinutes); + await f.journal.admit(call.id, f.deploymentId, afterFiveMinutes); + const dispatch = await f.journal.dispatch( + call.id, + { + bootId: "boot", + intentId: crypto.randomUUID(), + expiresAt: new Date(afterFiveMinutes.getTime() + 1000), + }, + afterFiveMinutes, + ); + expect(dispatch.state).toBe("dispatching"); + await f.journal.expire(f.deploymentId, new Date(afterFiveMinutes.getTime() + 61_000)); + expect((await f.journal.get(call.id))!.state).toBe("outcome_unknown"); + } finally { + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)("replicas share connection and deployment admission ceilings", async () => { + const fixtures: Awaited>[] = []; + const auth = { + kind: "automation" as const, + grant: { + upstreamName: tool.upstreamName, + automationWrite: true, + argumentConstraints: {}, + unrestricted: true, + }, + }; + try { + for (let i = 0; i < 9; i++) fixtures.push(await fixture()); + const first = fixtures[0]!; + const allCalls = await Promise.all( + fixtures.flatMap((f) => + Array.from({ length: 4 }, (_, i) => + f.journal.create({ ...f.input, toolCallId: `limit-${i}` }, auth, f.now), + ), + ), + ); + const states = await Promise.all( + allCalls.map((c) => makeMcpJournal(first.db).admit(c.id, first.deploymentId, first.now)), + ); + expect(states.filter((state) => state === "admitted")).toHaveLength(32); + expect(states.filter((state) => state === "queued")).toHaveLength(4); + // Clear the deployment test through normal task retention. + for (const f of fixtures) await f.db.delete(task).where(eq(task.id, f.input.taskId)); + await first.db + .insert(task) + .values({ id: first.input.taskId, type: "chat", createdByUserId: "owner" }); + const sessions = [first.input.sessionId, crypto.randomUUID(), crypto.randomUUID()]; + for (const sessionId of sessions) + await first.db.insert(taskSession).values({ taskId: first.input.taskId, sessionId }); + const connectionCalls = await Promise.all( + sessions.flatMap((sessionId) => + Array.from({ length: 4 }, (_, i) => + first.journal.create( + { ...first.input, sessionId, toolCallId: `connection-${i}` }, + auth, + first.now, + ), + ), + ), + ); + const connectionStates = await Promise.all( + connectionCalls.map((c) => + makeMcpJournal(first.db).admit(c.id, first.deploymentId, first.now), + ), + ); + expect(connectionStates.filter((state) => state === "admitted")).toHaveLength(8); + expect(connectionStates.filter((state) => state === "queued")).toHaveLength(4); + } finally { + // Session-owned call payloads and child rows use the existing task retention path. + for (const f of fixtures) await f.db.delete(task).where(eq(task.id, f.input.taskId)); + for (const f of fixtures) await f.cleanup(); + } +}); + +test.skipIf(!reachable)( + "credential completion cannot authorize a connection edited during the flow", + async () => { + const f = await fixture(); + try { + await f.repo.updateConfig( + f.row.id, + 1, + { alias: f.row.alias, displayName: "Edited", config }, + f.now, + ); + await expect( + f.repo.setAuthorization(f.row.id, 1, f.input.authGeneration, "account-b", f.now), + ).rejects.toThrow("stale"); + } finally { + await f.cleanup(); + } + }, +); diff --git a/orchestrator/src/__tests__/profiles.test.ts b/orchestrator/src/__tests__/profiles.test.ts index 2cbff58cd..9946fffdc 100644 --- a/orchestrator/src/__tests__/profiles.test.ts +++ b/orchestrator/src/__tests__/profiles.test.ts @@ -766,3 +766,22 @@ describe("user-scoped grants (ADR 0115)", () => { } finally { await s.close(); } }); }); + +describe("MCP profile grant contract", () => { + test("create, update, and list preserve explicit organization scope and constraints", async () => { + const s = await spawn({ getSession: makeGetSession("admin", "admin"), store: makeFakeStore(), images: fakeImages(["img-1"]), mountCatalog: fakeCatalog([]) }); + const grant = { connectionId: "default-mcp", operation: "invoke", credentialScope: "org", resourceConstraints: [], mcp: { upstreamName: "Create.Record", automationWrite: true, argumentConstraintsJson: JSON.stringify({ type: "object", properties: { project: { const: "allowed" } }, required: ["project"] }), unrestricted: false } }; + const input = { name: "Tools", imageId: "img-1", harness: "claude", integrationGrants: [grant] }; + try { + const created = await s.client.createProfile(input); + expect(created.profile!.integrationGrants[0]!.credentialScope).toBe("org"); + expect(created.profile!.integrationGrants[0]!.mcp?.argumentConstraintsJson).toBe(grant.mcp.argumentConstraintsJson); + const updated = await s.client.updateProfile({ ...input, id: created.profile!.id, name: "Renamed tools" }); + expect(updated.profile!.integrationGrants[0]!.mcp?.automationWrite).toBe(true); + const listed = await s.client.listProfiles({}); + expect(listed.profiles[0]!.integrationGrants[0]!.mcp?.upstreamName).toBe("Create.Record"); + await expectErr(s.client.updateProfile({ ...input, id: created.profile!.id, integrationGrants: [{ ...grant, credentialScope: "user" }] }), Code.InvalidArgument); + await expectErr(s.client.updateProfile({ ...input, id: created.profile!.id, integrationGrants: [{ ...grant, mcp: { ...grant.mcp, argumentConstraintsJson: "{}" } }] }), Code.InvalidArgument); + } finally { await s.close(); } + }); +}); diff --git a/orchestrator/src/db/integration-connections.ts b/orchestrator/src/db/integration-connections.ts index c9a0c7c88..0e0abcfc3 100644 --- a/orchestrator/src/db/integration-connections.ts +++ b/orchestrator/src/db/integration-connections.ts @@ -1,6 +1,7 @@ /** Named integration connection store (ADR 0109). */ -import { and, asc, eq, inArray } from "drizzle-orm"; +import { validateMcpConfig } from "../mcp/config.ts"; +import { and, asc, eq, inArray, ne } from "drizzle-orm"; import { getDb } from "./client.ts"; import { integrationConnection as connectionTable } from "./schema.ts"; @@ -14,6 +15,7 @@ export interface GoogleCloudConnectionConfig { } export interface IntegrationConnectionRow { + mcp?: import("../mcp/types.ts").McpConnectionState; id: string; alias: string; provider: string; @@ -53,6 +55,15 @@ export interface IntegrationConnectionStore { function toRow(r: typeof connectionTable.$inferSelect): IntegrationConnectionRow { return { + ...(r.provider === "mcp" ? { + mcp: { + configRevision: r.configRevision, + authGeneration: r.authGeneration, + credentialIdentity: r.credentialIdentity, + catalogRevision: r.catalogRevision, + revoked: r.revoked, + }, + } : {}), id: r.id, alias: r.alias, provider: r.provider, @@ -107,6 +118,7 @@ export function makeIntegrationConnectionStore( getDefault, async create(input) { + if (input.provider === "mcp") input = { ...input, config: validateMcpConfig(input.config) }; const rows = await db .insert(connectionTable) .values({ id: crypto.randomUUID(), ...input, enabled: false }) @@ -118,7 +130,7 @@ export function makeIntegrationConnectionStore( const rows = await db .update(connectionTable) .set({ ...input, enabled: false, testedAt: null, updatedAt: new Date() }) - .where(eq(connectionTable.id, id)) + .where(and(eq(connectionTable.id, id), ne(connectionTable.provider, "mcp"))) .returning(); return rows[0] ? toRow(rows[0]) : null; }, @@ -135,7 +147,7 @@ export function makeIntegrationConnectionStore( const rows = await db .update(connectionTable) .set({ testedAt, updatedAt: testedAt }) - .where(eq(connectionTable.id, id)) + .where(and(eq(connectionTable.id, id), ne(connectionTable.provider, "mcp"))) .returning(); return rows[0] ? toRow(rows[0]) : null; }, @@ -144,7 +156,7 @@ export function makeIntegrationConnectionStore( const rows = await db .update(connectionTable) .set({ enabled, updatedAt: new Date() }) - .where(eq(connectionTable.id, id)) + .where(and(eq(connectionTable.id, id), ne(connectionTable.provider, "mcp"))) .returning(); return rows[0] ? toRow(rows[0]) : null; }, @@ -153,12 +165,13 @@ export function makeIntegrationConnectionStore( const rows = await db .update(connectionTable) .set({ config, updatedAt: new Date() }) - .where(eq(connectionTable.id, id)) + .where(and(eq(connectionTable.id, id), ne(connectionTable.provider, "mcp"))) .returning(); return rows[0] ? toRow(rows[0]) : null; }, async ensureDefault(provider, displayName) { + if (provider === "mcp") throw new Error("MCP requires an explicit named connection"); const existing = await getDefault(provider); if (existing) return existing; await db.insert(connectionTable).values({ diff --git a/orchestrator/src/db/schema.ts b/orchestrator/src/db/schema.ts index 116a24488..a6fab8d78 100644 --- a/orchestrator/src/db/schema.ts +++ b/orchestrator/src/db/schema.ts @@ -1155,6 +1155,7 @@ export interface ProfileIntegrationGrant { * while programmatic sessions always compile the org credential. Rides the * grant array through launch_policy and the task_session snapshot. */ credentialScope?: "org" | "user"; + mcp?: import("../mcp/types.ts").McpToolGrant; } /** A parsed forge remote — enough to join against the GitHub integration. */ @@ -1175,6 +1176,7 @@ export interface ProfileRepo { /** Immutable non-secret connection configuration stamped onto a session. */ export interface IntegrationConnectionSnapshot { + mcp?: import("../mcp/types.ts").McpBinding; id: string; alias: string; provider: string; @@ -1743,6 +1745,11 @@ export const integrationConnection = pgTable( { id: text("id").primaryKey(), alias: text("alias").notNull().unique(), + configRevision: integer("config_revision").notNull().default(1), + authGeneration: integer("auth_generation").notNull().default(1), + credentialIdentity: text("credential_identity").notNull().default(""), + catalogRevision: integer("catalog_revision").notNull().default(0), + revoked: boolean("revoked").notNull().default(false), provider: text("provider").notNull(), displayName: text("display_name").notNull(), isDefault: boolean("is_default").notNull().default(false), @@ -2155,3 +2162,176 @@ export const accountRelations = relations(account, ({ one }) => ({ references: [user.id], }), })); + +/** Immutable discovery and review records. Published revisions never change. */ +export const mcpCatalog = pgTable( + "mcp_catalog", + { + id: text("id").primaryKey(), + connectionId: text("connection_id") + .notNull() + .references(() => integrationConnection.id), + kind: text("kind").$type<"draft" | "published">().notNull(), + revision: integer("revision").notNull(), + configRevision: integer("config_revision").notNull(), + authGeneration: integer("auth_generation").notNull(), + credentialIdentity: text("credential_identity").notNull(), + tools: jsonb("tools") + .$type< + import("../mcp/types.ts").McpDiscoveredTool[] | import("../mcp/types.ts").McpReviewedTool[] + >() + .notNull(), + fingerprint: text("fingerprint").notNull(), + actorId: text("actor_id").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull(), + }, + (t) => [ + index("mcp_catalog_connection_idx").on(t.connectionId, t.createdAt), + uniqueIndex("mcp_catalog_published_revision_idx") + .on(t.connectionId, t.revision) + .where(sql`kind = 'published'`), + check( + "mcp_catalog_kind_check", + sql`${t.kind} IN ('draft','published') AND ((${t.kind} = 'draft' AND ${t.revision} = 0) OR (${t.kind} = 'published' AND ${t.revision} > 0))`, + ), + ], +); + +/** Authoritative upstream outcome. Guest completion is a separate projection. */ +export const mcpExecution = pgTable( + "mcp_execution", + { + id: text("id").primaryKey(), + taskId: text("task_id").notNull(), + sessionId: text("session_id").notNull(), + runId: text("run_id").notNull(), + toolCallId: text("tool_call_id").notNull(), + connectionId: text("connection_id") + .notNull() + .references(() => integrationConnection.id), + catalogId: text("catalog_id") + .notNull() + .references(() => mcpCatalog.id), + principalId: text("principal_id").notNull(), + bindingHash: text("binding_hash").notNull(), + requestDigest: text("request_digest").notNull(), + argumentHash: text("argument_hash").notNull(), + upstreamName: text("upstream_name").notNull(), + configRevision: integer("config_revision").notNull(), + authGeneration: integer("auth_generation").notNull(), + classification: text("classification").$type<"read" | "write">().notNull(), + authorizationKind: text("authorization_kind") + .$type<"read" | "approval" | "automation">() + .notNull(), + automationGrantHash: text("automation_grant_hash"), + state: text("state").$type().notNull(), + version: integer("version").notNull().default(1), + bootId: text("boot_id"), + intentId: text("intent_id"), + intentExpiresAt: timestamp("intent_expires_at", { withTimezone: true }), + deadline: timestamp("deadline", { withTimezone: true }).notNull(), + result: jsonb("result").$type>(), + resultUnavailable: boolean("result_unavailable").notNull().default(false), + createdAt: timestamp("created_at", { withTimezone: true }).notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(), + }, + (t) => [ + foreignKey({ + columns: [t.taskId, t.sessionId], + foreignColumns: [taskSession.taskId, taskSession.sessionId], + }).onDelete("cascade"), + index("mcp_execution_task_session_idx").on(t.taskId, t.sessionId), + uniqueIndex("mcp_execution_call_idx").on(t.sessionId, t.toolCallId), + index("mcp_execution_connection_state_idx").on(t.connectionId, t.sessionId, t.state), + index("mcp_execution_catalog_idx").on(t.catalogId), + index("mcp_execution_pending_idx").on(t.state, t.deadline), + uniqueIndex("mcp_execution_intent_idx").on(t.intentId), + check( + "mcp_execution_state_check", + sql`${t.state} IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')`, + ), + check("mcp_execution_classification_check", sql`${t.classification} IN ('read','write')`), + check( + "mcp_execution_authorization_check", + sql`(${t.authorizationKind} = 'read' AND ${t.classification} = 'read' AND ${t.automationGrantHash} IS NULL) OR (${t.authorizationKind} = 'approval' AND ${t.automationGrantHash} IS NULL) OR (${t.authorizationKind} = 'automation' AND ${t.classification} = 'write' AND ${t.automationGrantHash} IS NOT NULL)`, + ), + check( + "mcp_execution_dispatch_check", + sql`${t.state} NOT IN ('dispatching','outcome_unknown','succeeded') OR (${t.bootId} IS NOT NULL AND ${t.intentId} IS NOT NULL AND ${t.intentExpiresAt} IS NOT NULL)`, + ), + ], +); + +export const mcpApproval = pgTable( + "mcp_approval", + { + executionId: text("execution_id") + .primaryKey() + .references(() => mcpExecution.id, { onDelete: "cascade" }), + deploymentId: text("deployment_id").notNull(), + sessionId: text("session_id").notNull(), + requestDigest: text("request_digest").notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + decision: text("decision").$type<"pending" | "approved" | "denied" | "expired">().notNull(), + actorId: text("actor_id"), + decidedAt: timestamp("decided_at", { withTimezone: true }), + consumedAt: timestamp("consumed_at", { withTimezone: true }), + }, + (t) => [ + index("mcp_approval_pending_idx").on(t.deploymentId, t.decision, t.expiresAt), + index("mcp_approval_session_idx").on(t.sessionId, t.decision), + check( + "mcp_approval_decision_check", + sql`${t.decision} IN ('pending','approved','denied','expired')`, + ), + check( + "mcp_approval_consumed_check", + sql`${t.consumedAt} IS NULL OR (${t.decision} = 'approved' AND ${t.actorId} IS NOT NULL)`, + ), + ], +); + +export const mcpResolution = pgTable( + "mcp_resolution", + { + executionId: text("execution_id") + .primaryKey() + .references(() => mcpExecution.id, { onDelete: "cascade" }), + actorId: text("actor_id").notNull(), + outcome: text("outcome").$type<"succeeded" | "failed" | "acknowledged">().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull(), + }, + (t) => [ + check( + "mcp_resolution_outcome_check", + sql`${t.outcome} IN ('succeeded','failed','acknowledged')`, + ), + ], +); + +/** Durable row locks serialize short admission decisions across replicas. */ +export const mcpAdmissionScope = pgTable("mcp_admission_scope", { id: text("id").primaryKey() }); +export const mcpAdmission = pgTable( + "mcp_admission", + { + executionId: text("execution_id") + .primaryKey() + .references(() => mcpExecution.id, { onDelete: "cascade" }), + deploymentId: text("deployment_id") + .notNull() + .references(() => mcpAdmissionScope.id), + connectionId: text("connection_id") + .notNull() + .references(() => integrationConnection.id), + sessionId: text("session_id").notNull(), + state: text("state").$type<"queued" | "admitted">().notNull(), + expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull(), + }, + (t) => [ + index("mcp_admission_deployment_idx").on(t.deploymentId, t.state, t.expiresAt), + index("mcp_admission_connection_idx").on(t.connectionId, t.state), + index("mcp_admission_session_idx").on(t.sessionId, t.state), + check("mcp_admission_state_check", sql`${t.state} IN ('queued','admitted')`), + ], +); diff --git a/orchestrator/src/gen/engram/app/v1/integration_pb.ts b/orchestrator/src/gen/engram/app/v1/integration_pb.ts index aa87250b6..acd7513f6 100644 --- a/orchestrator/src/gen/engram/app/v1/integration_pb.ts +++ b/orchestrator/src/gen/engram/app/v1/integration_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/integration.proto. */ export const file_engram_app_v1_integration: GenFile = /*@__PURE__*/ - fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAki/AEKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIpIBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWciVAoYQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiKMAQoXVXBkYXRlQ29ubmVjdGlvblJlcXVlc3QSCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnIlQKGFVwZGF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24iJQoXRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QSCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSI6ChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCCJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IynQwKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZWIGcHJvdG8z"); + fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i2AEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAYgASgNEgoKAmlkGAEgASgJEg0KBWFsaWFzGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRJACgxnb29nbGVfY2xvdWQYBCABKAsyKi5lbmdyYW0uYXBwLnYxLkdvb2dsZUNsb3VkQ29ubmVjdGlvbkNvbmZpZxIvCgNtY3AYBSABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENvbm5lY3Rpb25Db25maWciVAoYVXBkYXRlQ29ubmVjdGlvblJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIlChdEZWxldGVDb25uZWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCSIrChhEZWxldGVDb25uZWN0aW9uUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCIjChVUZXN0Q29ubmVjdGlvblJlcXVlc3QSCgoCaWQYASABKAkiNQoWVGVzdENvbm5lY3Rpb25SZXNwb25zZRIKCgJvaxgBIAEoCBIPCgdtZXNzYWdlGAIgASgJIlUKG1NldENvbm5lY3Rpb25FbmFibGVkUmVxdWVzdBIKCgJpZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNIlgKHFNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIigKGkdldEdvb2dsZUNsb3VkU2V0dXBSZXF1ZXN0EgoKAmlkGAEgASgJIqIBChtHZXRHb29nbGVDbG91ZFNldHVwUmVzcG9uc2USDgoGaXNzdWVyGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEhkKEXN1YmplY3RfYXR0cmlidXRlGAMgASgJEhwKFGNvbm5lY3Rpb25fYXR0cmlidXRlGAQgASgJEhUKDWdjbG91ZF9zY3JpcHQYBSABKAkSEQoJdGVycmFmb3JtGAYgASgJIuYBCglDb25uZWN0b3ISEAoIcHJvdmlkZXIYASABKAkSEwoLY29uZmlnX2pzb24YAiABKAkSDwoHYnVpbHRpbhgDIAEoCBISCgpjcmVhdGVkX2F0GAQgASgJEhIKCnVwZGF0ZWRfYXQYBSABKAkSDgoGc3RhdHVzGAYgASgJEjgKCHNldHRpbmdzGAcgAygLMiYuZW5ncmFtLmFwcC52MS5Db25uZWN0b3IuU2V0dGluZ3NFbnRyeRovCg1TZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiFwoVTGlzdENvbm5lY3RvcnNSZXF1ZXN0IkYKFkxpc3RDb25uZWN0b3JzUmVzcG9uc2USLAoKY29ubmVjdG9ycxgBIAMoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIi0KFlVwc2VydENvbm5lY3RvclJlcXVlc3QSEwoLY29uZmlnX2pzb24YASABKAkiRgoXVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiKgoWRGVsZXRlQ29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCSIqChdEZWxldGVDb25uZWN0b3JSZXNwb25zZRIPCgdkZWxldGVkGAEgASgIIjoKDUNvbm5lY3Rvckljb24SDAoEbW9ubxgBIAEoCRINCgVjb2xvchgCIAEoCRIMCgRsb2dvGAMgASgJIn8KEENvbm5lY3RvckRpc3BsYXkSDAoEbmFtZRgBIAEoCRIQCghjYXRlZ29yeRgCIAEoCRINCgVibHVyYhgDIAEoCRIqCgRpY29uGAQgASgLMhwuZW5ncmFtLmFwcC52MS5Db25uZWN0b3JJY29uEhAKCGZlYXR1cmVkGAUgASgIInYKEUNhdGFsb2dDYXBhYmlsaXR5Eg4KBmFjdGlvbhgBIAEoCRIOCgZhY2Nlc3MYAiABKAkSDQoFYXNzZXQYAyABKAkSDQoFbGFiZWwYBCABKAkSDAoEaG9zdBgFIAEoCRIVCg1lbmRwb2ludF9ydWxlGAYgASgJIrQCChRQcm92aWRlckNhdGFsb2dFbnRyeRIQCghwcm92aWRlchgBIAEoCRIwCgdkaXNwbGF5GAIgASgLMh8uZW5ncmFtLmFwcC52MS5Db25uZWN0b3JEaXNwbGF5EhkKEWNyZWRlbnRpYWxfc291cmNlGAMgASgJEg0KBWhvc3RzGAQgAygJEjYKDGNhcGFiaWxpdGllcxgFIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ2F0YWxvZ0NhcGFiaWxpdHkSHQoVZGVmYXVsdF9jb25uZWN0aW9uX2lkGAYgASgJEhgKEGNvbm5lY3Rpb25fbW9kZWwYByABKAkSPQoPdXNlcl9jcmVkZW50aWFsGAggASgLMiQuZW5ncmFtLmFwcC52MS5Vc2VyQ3JlZGVudGlhbFN1cHBvcnQiSQoVVXNlckNyZWRlbnRpYWxTdXBwb3J0Eg0KBW9hdXRoGAEgASgIEg0KBXRva2VuGAIgASgIEhIKCnRva2VuX2hpbnQYAyABKAkiHgocR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVxdWVzdCJXCh1HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXNwb25zZRI2Cglwcm92aWRlcnMYASADKAsyIy5lbmdyYW0uYXBwLnYxLlByb3ZpZGVyQ2F0YWxvZ0VudHJ5Iq4BChhTZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkSDAoEa2luZBgCIAEoCRJDCgZ2YWx1ZXMYAyADKAsyMy5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVxdWVzdC5WYWx1ZXNFbnRyeRotCgtWYWx1ZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjEKGVNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USFAoMc2VjcmV0X25hbWVzGAEgAygJIlAKGlVwbG9hZENvbm5lY3RvckxvZ29SZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGRhdGEYAiABKAwSEgoKbWVkaWFfdHlwZRgDIAEoCSIvChtVcGxvYWRDb25uZWN0b3JMb2dvUmVzcG9uc2USEAoIbG9nb191cmwYASABKAkirgIKFFRlc3RDb25uZWN0b3JSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEkoKDGRyYWZ0X3ZhbHVlcxgCIAMoCzI0LmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QuRHJhZnRWYWx1ZXNFbnRyeRJOCg5kcmFmdF9zZXR0aW5ncxgDIAMoCzI2LmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QuRHJhZnRTZXR0aW5nc0VudHJ5GjIKEERyYWZ0VmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ARo0ChJEcmFmdFNldHRpbmdzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI0ChVUZXN0Q29ubmVjdG9yUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSKsAQobU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEkoKCHNldHRpbmdzGAIgAygLMjguZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QuU2V0dGluZ3NFbnRyeRovCg1TZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiSwocU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXNwb25zZRIrCgljb25uZWN0b3IYASABKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciKrAQoTTWNwQ29ubmVjdGlvbkNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIMCgR6b25lGAIgASgJEhEKCWF1dGhfa2luZBgDIAEoCRIaChJhcGlfa2V5X3NlY3JldF9yZWYYBCABKAkSJAocb2F1dGhfY2xpZW50X3JlZ2lzdHJhdGlvbl9pZBgFIAEoCRIfChdzaGFyZV93aXRoX29yZ19zZXNzaW9ucxgGIAEoCCKqAQoHTWNwVG9vbBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhkKEWlucHV0X3NjaGVtYV9qc29uGAMgASgJEhoKEm91dHB1dF9zY2hlbWFfanNvbhgEIAEoCRIWCg5jbGFzc2lmaWNhdGlvbhgFIAEoCRIPCgdlbmFibGVkGAYgASgIEhMKC2ZpbmdlcnByaW50GAcgASgJIr0BCgpNY3BDYXRhbG9nEgoKAmlkGAEgASgJEhUKDWNvbm5lY3Rpb25faWQYAiABKAkSDAoEa2luZBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoDRIXCg9jb25maWdfcmV2aXNpb24YBSABKA0SFwoPYXV0aF9nZW5lcmF0aW9uGAYgASgNEiUKBXRvb2xzGAcgAygLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEhMKC2ZpbmdlcnByaW50GAggASgJIkYKEkRpc2NvdmVyTWNwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNIkEKE0Rpc2NvdmVyTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyJ+ChFQdWJsaXNoTWNwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhAKCGRyYWZ0X2lkGAIgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEiUKBXRvb2xzGAQgAygLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sIkAKElB1Ymxpc2hNY3BSZXNwb25zZRIqCgdjYXRhbG9nGAEgASgLMhkuZW5ncmFtLmFwcC52MS5NY3BDYXRhbG9nImoKE0F1dGhvcml6ZU1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAIgASgNEh8KF3NoYXJlX3dpdGhfb3JnX3Nlc3Npb25zGAMgASgIIjEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJIkoKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIs4BCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkiLQoXTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJJChhMaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USLQoJYXBwcm92YWxzGAEgAygLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbCJBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgiKgoZRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRINCgVzdGF0ZRgBIAEoCSIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAky3BIKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USVwoMQXV0aG9yaXplTWNwEiIuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXNwb25zZRJaCg1EaXNjb25uZWN0TWNwEiMuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGlzY29ubmVjdE1jcFJlc3BvbnNlEmMKEExpc3RNY3BBcHByb3ZhbHMSJi5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5MaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USZgoRRGVjaWRlTWNwQXBwcm92YWwSJy5lbmdyYW0uYXBwLnYxLkRlY2lkZU1jcEFwcHJvdmFsUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRJaCg1DYW5jZWxNY3BDYWxsEiMuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEmYKEVJlc29sdmVNY3BPdXRjb21lEicuZW5ncmFtLmFwcC52MS5SZXNvbHZlTWNwT3V0Y29tZVJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVzcG9uc2USTgoJUmViaW5kTWNwEh8uZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXNwb25zZWIGcHJvdG8z"); /** * @generated from message engram.app.v1.GoogleCloudConnectionConfig @@ -106,6 +106,36 @@ export type IntegrationConnection = Message<"engram.app.v1.IntegrationConnection * @generated from field: bool is_default = 10; */ isDefault: boolean; + + /** + * @generated from field: engram.app.v1.McpConnectionConfig mcp = 11; + */ + mcp?: McpConnectionConfig; + + /** + * @generated from field: uint32 config_revision = 12; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 13; + */ + authGeneration: number; + + /** + * @generated from field: string credential_identity = 14; + */ + credentialIdentity: string; + + /** + * @generated from field: uint32 catalog_revision = 15; + */ + catalogRevision: number; + + /** + * @generated from field: bool revoked = 16; + */ + revoked: boolean; }; /** @@ -168,6 +198,11 @@ export type CreateConnectionRequest = Message<"engram.app.v1.CreateConnectionReq * @generated from field: engram.app.v1.GoogleCloudConnectionConfig google_cloud = 4; */ googleCloud?: GoogleCloudConnectionConfig; + + /** + * @generated from field: engram.app.v1.McpConnectionConfig mcp = 5; + */ + mcp?: McpConnectionConfig; }; /** @@ -198,6 +233,11 @@ export const CreateConnectionResponseSchema: GenMessage & { + /** + * @generated from field: uint32 expected_revision = 6; + */ + expectedRevision: number; + /** * @generated from field: string id = 1; */ @@ -217,6 +257,11 @@ export type UpdateConnectionRequest = Message<"engram.app.v1.UpdateConnectionReq * @generated from field: engram.app.v1.GoogleCloudConnectionConfig google_cloud = 4; */ googleCloud?: GoogleCloudConnectionConfig; + + /** + * @generated from field: engram.app.v1.McpConnectionConfig mcp = 5; + */ + mcp?: McpConnectionConfig; }; /** @@ -329,6 +374,11 @@ export type SetConnectionEnabledRequest = Message<"engram.app.v1.SetConnectionEn * @generated from field: bool enabled = 2; */ enabled: boolean; + + /** + * @generated from field: uint32 expected_revision = 3; + */ + expectedRevision: number; }; /** @@ -1034,6 +1084,564 @@ export type SetConnectorSettingsResponse = Message<"engram.app.v1.SetConnectorSe export const SetConnectorSettingsResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_integration, 37); +/** + * Organization credential references only. Secret values never enter this contract. + * + * @generated from message engram.app.v1.McpConnectionConfig + */ +export type McpConnectionConfig = Message<"engram.app.v1.McpConnectionConfig"> & { + /** + * @generated from field: string endpoint = 1; + */ + endpoint: string; + + /** + * @generated from field: string zone = 2; + */ + zone: string; + + /** + * @generated from field: string auth_kind = 3; + */ + authKind: string; + + /** + * @generated from field: string api_key_secret_ref = 4; + */ + apiKeySecretRef: string; + + /** + * @generated from field: string oauth_client_registration_id = 5; + */ + oauthClientRegistrationId: string; + + /** + * @generated from field: bool share_with_org_sessions = 6; + */ + shareWithOrgSessions: boolean; +}; + +/** + * Describes the message engram.app.v1.McpConnectionConfig. + * Use `create(McpConnectionConfigSchema)` to create a new message. + */ +export const McpConnectionConfigSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 38); + +/** + * @generated from message engram.app.v1.McpTool + */ +export type McpTool = Message<"engram.app.v1.McpTool"> & { + /** + * @generated from field: string upstream_name = 1; + */ + upstreamName: string; + + /** + * @generated from field: string description = 2; + */ + description: string; + + /** + * @generated from field: string input_schema_json = 3; + */ + inputSchemaJson: string; + + /** + * @generated from field: string output_schema_json = 4; + */ + outputSchemaJson: string; + + /** + * Empty in discovery drafts. Publication requires explicit read or write. + * + * @generated from field: string classification = 5; + */ + classification: string; + + /** + * @generated from field: bool enabled = 6; + */ + enabled: boolean; + + /** + * @generated from field: string fingerprint = 7; + */ + fingerprint: string; +}; + +/** + * Describes the message engram.app.v1.McpTool. + * Use `create(McpToolSchema)` to create a new message. + */ +export const McpToolSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 39); + +/** + * @generated from message engram.app.v1.McpCatalog + */ +export type McpCatalog = Message<"engram.app.v1.McpCatalog"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string connection_id = 2; + */ + connectionId: string; + + /** + * @generated from field: string kind = 3; + */ + kind: string; + + /** + * @generated from field: uint32 revision = 4; + */ + revision: number; + + /** + * @generated from field: uint32 config_revision = 5; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 6; + */ + authGeneration: number; + + /** + * @generated from field: repeated engram.app.v1.McpTool tools = 7; + */ + tools: McpTool[]; + + /** + * @generated from field: string fingerprint = 8; + */ + fingerprint: string; +}; + +/** + * Describes the message engram.app.v1.McpCatalog. + * Use `create(McpCatalogSchema)` to create a new message. + */ +export const McpCatalogSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 40); + +/** + * @generated from message engram.app.v1.DiscoverMcpRequest + */ +export type DiscoverMcpRequest = Message<"engram.app.v1.DiscoverMcpRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; +}; + +/** + * Describes the message engram.app.v1.DiscoverMcpRequest. + * Use `create(DiscoverMcpRequestSchema)` to create a new message. + */ +export const DiscoverMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 41); + +/** + * @generated from message engram.app.v1.DiscoverMcpResponse + */ +export type DiscoverMcpResponse = Message<"engram.app.v1.DiscoverMcpResponse"> & { + /** + * @generated from field: engram.app.v1.McpCatalog catalog = 1; + */ + catalog?: McpCatalog; +}; + +/** + * Describes the message engram.app.v1.DiscoverMcpResponse. + * Use `create(DiscoverMcpResponseSchema)` to create a new message. + */ +export const DiscoverMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 42); + +/** + * @generated from message engram.app.v1.PublishMcpRequest + */ +export type PublishMcpRequest = Message<"engram.app.v1.PublishMcpRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: string draft_id = 2; + */ + draftId: string; + + /** + * @generated from field: uint32 expected_revision = 3; + */ + expectedRevision: number; + + /** + * @generated from field: repeated engram.app.v1.McpTool tools = 4; + */ + tools: McpTool[]; +}; + +/** + * Describes the message engram.app.v1.PublishMcpRequest. + * Use `create(PublishMcpRequestSchema)` to create a new message. + */ +export const PublishMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 43); + +/** + * @generated from message engram.app.v1.PublishMcpResponse + */ +export type PublishMcpResponse = Message<"engram.app.v1.PublishMcpResponse"> & { + /** + * @generated from field: engram.app.v1.McpCatalog catalog = 1; + */ + catalog?: McpCatalog; +}; + +/** + * Describes the message engram.app.v1.PublishMcpResponse. + * Use `create(PublishMcpResponseSchema)` to create a new message. + */ +export const PublishMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 44); + +/** + * @generated from message engram.app.v1.AuthorizeMcpRequest + */ +export type AuthorizeMcpRequest = Message<"engram.app.v1.AuthorizeMcpRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_generation = 2; + */ + expectedGeneration: number; + + /** + * @generated from field: bool share_with_org_sessions = 3; + */ + shareWithOrgSessions: boolean; +}; + +/** + * Describes the message engram.app.v1.AuthorizeMcpRequest. + * Use `create(AuthorizeMcpRequestSchema)` to create a new message. + */ +export const AuthorizeMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 45); + +/** + * @generated from message engram.app.v1.AuthorizeMcpResponse + */ +export type AuthorizeMcpResponse = Message<"engram.app.v1.AuthorizeMcpResponse"> & { + /** + * @generated from field: string authorization_url = 1; + */ + authorizationUrl: string; +}; + +/** + * Describes the message engram.app.v1.AuthorizeMcpResponse. + * Use `create(AuthorizeMcpResponseSchema)` to create a new message. + */ +export const AuthorizeMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 46); + +/** + * @generated from message engram.app.v1.DisconnectMcpRequest + */ +export type DisconnectMcpRequest = Message<"engram.app.v1.DisconnectMcpRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_generation = 2; + */ + expectedGeneration: number; +}; + +/** + * Describes the message engram.app.v1.DisconnectMcpRequest. + * Use `create(DisconnectMcpRequestSchema)` to create a new message. + */ +export const DisconnectMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 47); + +/** + * @generated from message engram.app.v1.DisconnectMcpResponse + */ +export type DisconnectMcpResponse = Message<"engram.app.v1.DisconnectMcpResponse"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; +}; + +/** + * Describes the message engram.app.v1.DisconnectMcpResponse. + * Use `create(DisconnectMcpResponseSchema)` to create a new message. + */ +export const DisconnectMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 48); + +/** + * @generated from message engram.app.v1.McpApproval + */ +export type McpApproval = Message<"engram.app.v1.McpApproval"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 4; + */ + toolCallId: string; + + /** + * @generated from field: string connection_id = 5; + */ + connectionId: string; + + /** + * @generated from field: string upstream_name = 6; + */ + upstreamName: string; + + /** + * @generated from field: string request_digest = 7; + */ + requestDigest: string; + + /** + * @generated from field: string argument_hash = 8; + */ + argumentHash: string; + + /** + * @generated from field: string expires_at = 9; + */ + expiresAt: string; +}; + +/** + * Describes the message engram.app.v1.McpApproval. + * Use `create(McpApprovalSchema)` to create a new message. + */ +export const McpApprovalSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 49); + +/** + * @generated from message engram.app.v1.ListMcpApprovalsRequest + */ +export type ListMcpApprovalsRequest = Message<"engram.app.v1.ListMcpApprovalsRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; +}; + +/** + * Describes the message engram.app.v1.ListMcpApprovalsRequest. + * Use `create(ListMcpApprovalsRequestSchema)` to create a new message. + */ +export const ListMcpApprovalsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 50); + +/** + * @generated from message engram.app.v1.ListMcpApprovalsResponse + */ +export type ListMcpApprovalsResponse = Message<"engram.app.v1.ListMcpApprovalsResponse"> & { + /** + * @generated from field: repeated engram.app.v1.McpApproval approvals = 1; + */ + approvals: McpApproval[]; +}; + +/** + * Describes the message engram.app.v1.ListMcpApprovalsResponse. + * Use `create(ListMcpApprovalsResponseSchema)` to create a new message. + */ +export const ListMcpApprovalsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 51); + +/** + * @generated from message engram.app.v1.DecideMcpApprovalRequest + */ +export type DecideMcpApprovalRequest = Message<"engram.app.v1.DecideMcpApprovalRequest"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: bool approve = 2; + */ + approve: boolean; +}; + +/** + * Describes the message engram.app.v1.DecideMcpApprovalRequest. + * Use `create(DecideMcpApprovalRequestSchema)` to create a new message. + */ +export const DecideMcpApprovalRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 52); + +/** + * @generated from message engram.app.v1.DecideMcpApprovalResponse + */ +export type DecideMcpApprovalResponse = Message<"engram.app.v1.DecideMcpApprovalResponse"> & { + /** + * @generated from field: string state = 1; + */ + state: string; +}; + +/** + * Describes the message engram.app.v1.DecideMcpApprovalResponse. + * Use `create(DecideMcpApprovalResponseSchema)` to create a new message. + */ +export const DecideMcpApprovalResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 53); + +/** + * @generated from message engram.app.v1.CancelMcpCallRequest + */ +export type CancelMcpCallRequest = Message<"engram.app.v1.CancelMcpCallRequest"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; +}; + +/** + * Describes the message engram.app.v1.CancelMcpCallRequest. + * Use `create(CancelMcpCallRequestSchema)` to create a new message. + */ +export const CancelMcpCallRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 54); + +/** + * @generated from message engram.app.v1.CancelMcpCallResponse + */ +export type CancelMcpCallResponse = Message<"engram.app.v1.CancelMcpCallResponse"> & { + /** + * @generated from field: string state = 1; + */ + state: string; +}; + +/** + * Describes the message engram.app.v1.CancelMcpCallResponse. + * Use `create(CancelMcpCallResponseSchema)` to create a new message. + */ +export const CancelMcpCallResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 55); + +/** + * @generated from message engram.app.v1.ResolveMcpOutcomeRequest + */ +export type ResolveMcpOutcomeRequest = Message<"engram.app.v1.ResolveMcpOutcomeRequest"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string outcome = 2; + */ + outcome: string; +}; + +/** + * Describes the message engram.app.v1.ResolveMcpOutcomeRequest. + * Use `create(ResolveMcpOutcomeRequestSchema)` to create a new message. + */ +export const ResolveMcpOutcomeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 56); + +/** + * @generated from message engram.app.v1.ResolveMcpOutcomeResponse + */ +export type ResolveMcpOutcomeResponse = Message<"engram.app.v1.ResolveMcpOutcomeResponse"> & { + /** + * @generated from field: string outcome = 1; + */ + outcome: string; +}; + +/** + * Describes the message engram.app.v1.ResolveMcpOutcomeResponse. + * Use `create(ResolveMcpOutcomeResponseSchema)` to create a new message. + */ +export const ResolveMcpOutcomeResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 57); + +/** + * @generated from message engram.app.v1.RebindMcpRequest + */ +export type RebindMcpRequest = Message<"engram.app.v1.RebindMcpRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string expected_binding_hash = 2; + */ + expectedBindingHash: string; +}; + +/** + * Describes the message engram.app.v1.RebindMcpRequest. + * Use `create(RebindMcpRequestSchema)` to create a new message. + */ +export const RebindMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 58); + +/** + * @generated from message engram.app.v1.RebindMcpResponse + */ +export type RebindMcpResponse = Message<"engram.app.v1.RebindMcpResponse"> & { + /** + * @generated from field: string binding_hash = 1; + */ + bindingHash: string; +}; + +/** + * Describes the message engram.app.v1.RebindMcpResponse. + * Use `create(RebindMcpResponseSchema)` to create a new message. + */ +export const RebindMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 59); + /** * ADR 0057 C3: orchestrator-native connector catalog CRUD (Plane B). Connectors * live in the orchestrator DB (the `connector` table, C1); the built-ins @@ -1202,6 +1810,78 @@ export const IntegrationService: GenService<{ input: typeof GetGoogleCloudSetupRequestSchema; output: typeof GetGoogleCloudSetupResponseSchema; }, + /** + * @generated from rpc engram.app.v1.IntegrationService.DiscoverMcp + */ + discoverMcp: { + methodKind: "unary"; + input: typeof DiscoverMcpRequestSchema; + output: typeof DiscoverMcpResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.PublishMcp + */ + publishMcp: { + methodKind: "unary"; + input: typeof PublishMcpRequestSchema; + output: typeof PublishMcpResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.AuthorizeMcp + */ + authorizeMcp: { + methodKind: "unary"; + input: typeof AuthorizeMcpRequestSchema; + output: typeof AuthorizeMcpResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.DisconnectMcp + */ + disconnectMcp: { + methodKind: "unary"; + input: typeof DisconnectMcpRequestSchema; + output: typeof DisconnectMcpResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.ListMcpApprovals + */ + listMcpApprovals: { + methodKind: "unary"; + input: typeof ListMcpApprovalsRequestSchema; + output: typeof ListMcpApprovalsResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.DecideMcpApproval + */ + decideMcpApproval: { + methodKind: "unary"; + input: typeof DecideMcpApprovalRequestSchema; + output: typeof DecideMcpApprovalResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.CancelMcpCall + */ + cancelMcpCall: { + methodKind: "unary"; + input: typeof CancelMcpCallRequestSchema; + output: typeof CancelMcpCallResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.ResolveMcpOutcome + */ + resolveMcpOutcome: { + methodKind: "unary"; + input: typeof ResolveMcpOutcomeRequestSchema; + output: typeof ResolveMcpOutcomeResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.RebindMcp + */ + rebindMcp: { + methodKind: "unary"; + input: typeof RebindMcpRequestSchema; + output: typeof RebindMcpResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_engram_app_v1_integration, 0); diff --git a/orchestrator/src/gen/engram/app/v1/profile_pb.ts b/orchestrator/src/gen/engram/app/v1/profile_pb.ts index 1dfa02b2f..d939bc0ca 100644 --- a/orchestrator/src/gen/engram/app/v1/profile_pb.ts +++ b/orchestrator/src/gen/engram/app/v1/profile_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/profile.proto. */ export const file_engram_app_v1_profile: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8iewoXUHJvZmlsZUludGVncmF0aW9uR3JhbnQSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSHAoUcmVzb3VyY2VfY29uc3RyYWludHMYAyADKAkSGAoQY3JlZGVudGlhbF9zY29wZRgEIAEoCSJTCg5Qcm9maWxlTmV0d29yaxIPCgdkZWZhdWx0GAEgASgJEhMKC2FsbG93X2hvc3RzGAIgAygJEhsKE2FsbG93X2hvc3RfcGF0dGVybnMYAyADKAkibQoNUHJvZmlsZVNlY3JldBILCgNyZWYYASABKAkSDwoHZW52X3ZhchgCIAEoCRIMCgRtb2RlGAMgASgJEhMKC2FsbG93X2hvc3RzGAQgAygJEhsKE2FsbG93X2hvc3RfcGF0dGVybnMYBSADKAkibgoPUHJvZmlsZVNuYXBzaG90EgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghhcmNoaXZlZBgEIAEoCBIRCglpbWFnZV91cmkYBSABKAkSDgoGc2tpbGxzGAYgAygJIksKE0xpc3RQcm9maWxlc1JlcXVlc3QSGAoQaW5jbHVkZV9hcmNoaXZlZBgBIAEoCBIaChJmb3Jfc2Vzc2lvbl9waWNrZXIYAiABKAgiQAoUTGlzdFByb2ZpbGVzUmVzcG9uc2USKAoIcHJvZmlsZXMYASADKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiHwoRR2V0UHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiPQoSR2V0UHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUisAUKFENyZWF0ZVByb2ZpbGVSZXF1ZXN0EgwKBG5hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSDAoEaWNvbhgDIAEoCRIQCghpbWFnZV9pZBgEIAEoCRIbChNpbmNsdWRlX3VzZXJfdG9rZW5zGAUgASgIEkIKCGVudl92YXJzGAYgAygLMjAuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdC5FbnZWYXJzRW50cnkSDgoGc2tpbGxzGAcgAygJEi4KB25ldHdvcmsYCSABKAsyHS5lbmdyYW0uYXBwLnYxLlByb2ZpbGVOZXR3b3JrEi0KB3NlY3JldHMYCiADKAsyHC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVTZWNyZXQSFAoHaGFybmVzcxgMIAEoCUgAiAEBEhIKBW1vZGVsGA0gASgJSAGIAQESEwoGZWZmb3J0GA4gASgJSAKIAQESQgoSaW50ZWdyYXRpb25fZ3JhbnRzGBEgAygLMiYuZW5ncmFtLmFwcC52MS5Qcm9maWxlSW50ZWdyYXRpb25HcmFudBIpCgVyZXBvcxgSIAMoCzIaLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVJlcG8SGQoMbW9kZWxfcm91dGVyGBMgASgJSAOIAQESJwoEYXBwcxgUIAMoCzIZLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbkFwcBIZChFwcm9ncmFtbWF0aWNfb25seRgVIAEoCBouCgxFbnZWYXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfaGFybmVzc0IICgZfbW9kZWxCCQoHX2VmZm9ydEIPCg1fbW9kZWxfcm91dGVySgQICBAJSgQICxAMSgQIDxAQSgQIEBARIkAKFUNyZWF0ZVByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlItcFChRVcGRhdGVQcm9maWxlUmVxdWVzdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEgwKBGljb24YBCABKAkSEAoIaW1hZ2VfaWQYBSABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgGIAEoCBJCCghlbnZfdmFycxgHIAMoCzIwLmVuZ3JhbS5hcHAudjEuVXBkYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgIIAMoCRIuCgduZXR3b3JrGAogASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAsgAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDSABKAlIAIgBARISCgVtb2RlbBgOIAEoCUgBiAEBEhMKBmVmZm9ydBgPIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgSIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEyADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgUIAEoCUgDiAEBEicKBGFwcHMYFSADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASHgoRcHJvZ3JhbW1hdGljX29ubHkYFiABKAhIBIgBARouCgxFbnZWYXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfaGFybmVzc0IICgZfbW9kZWxCCQoHX2VmZm9ydEIPCg1fbW9kZWxfcm91dGVyQhQKEl9wcm9ncmFtbWF0aWNfb25seUoECAkQCkoECAwQDUoECBAQEUoECBEQEiJAChVVcGRhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSIiChREZWxldGVQcm9maWxlUmVxdWVzdBIKCgJpZBgBIAEoCSIXChVEZWxldGVQcm9maWxlUmVzcG9uc2UywQQKDlByb2ZpbGVTZXJ2aWNlElcKDExpc3RQcm9maWxlcxIiLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBojLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USUQoKR2V0UHJvZmlsZRIgLmVuZ3JhbS5hcHAudjEuR2V0UHJvZmlsZVJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXNwb25zZRJaCg1DcmVhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlc3BvbnNlEloKDVVwZGF0ZVByb2ZpbGUSIy5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVzcG9uc2USbwoURGlzY292ZXJQcm9maWxlUmVwb3MSKi5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRJaCg1EZWxldGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5EZWxldGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlc3BvbnNlYgZwcm90bzM"); + fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8ipQEKF1Byb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EhUKDWNvbm5lY3Rpb25faWQYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEhwKFHJlc291cmNlX2NvbnN0cmFpbnRzGAMgAygJEhgKEGNyZWRlbnRpYWxfc2NvcGUYBCABKAkSKAoDbWNwGAUgASgLMhsuZW5ncmFtLmFwcC52MS5NY3BUb29sR3JhbnQiUwoOUHJvZmlsZU5ldHdvcmsSDwoHZGVmYXVsdBgBIAEoCRITCgthbGxvd19ob3N0cxgCIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAMgAygJIm0KDVByb2ZpbGVTZWNyZXQSCwoDcmVmGAEgASgJEg8KB2Vudl92YXIYAiABKAkSDAoEbW9kZRgDIAEoCRITCgthbGxvd19ob3N0cxgEIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAUgAygJIm4KD1Byb2ZpbGVTbmFwc2hvdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGljb24YAyABKAkSEAoIYXJjaGl2ZWQYBCABKAgSEQoJaW1hZ2VfdXJpGAUgASgJEg4KBnNraWxscxgGIAMoCSJLChNMaXN0UHJvZmlsZXNSZXF1ZXN0EhgKEGluY2x1ZGVfYXJjaGl2ZWQYASABKAgSGgoSZm9yX3Nlc3Npb25fcGlja2VyGAIgASgIIkAKFExpc3RQcm9maWxlc1Jlc3BvbnNlEigKCHByb2ZpbGVzGAEgAygLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIh8KEUdldFByb2ZpbGVSZXF1ZXN0EgoKAmlkGAEgASgJIj0KEkdldFByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIrAFChRDcmVhdGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEgwKBGljb24YAyABKAkSEAoIaW1hZ2VfaWQYBCABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgFIAEoCBJCCghlbnZfdmFycxgGIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgHIAMoCRIuCgduZXR3b3JrGAkgASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAogAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDCABKAlIAIgBARISCgVtb2RlbBgNIAEoCUgBiAEBEhMKBmVmZm9ydBgOIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgRIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEiADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgTIAEoCUgDiAEBEicKBGFwcHMYFCADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASGQoRcHJvZ3JhbW1hdGljX29ubHkYFSABKAgaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSLXBQoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlKBAgJEApKBAgMEA1KBAgQEBFKBAgREBIiQAoVVXBkYXRlUHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiIgoURGVsZXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlUHJvZmlsZVJlc3BvbnNlIngKDE1jcFRvb2xHcmFudBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhgKEGF1dG9tYXRpb25fd3JpdGUYAiABKAgSIQoZYXJndW1lbnRfY29uc3RyYWludHNfanNvbhgDIAEoCRIUCgx1bnJlc3RyaWN0ZWQYBCABKAgywQQKDlByb2ZpbGVTZXJ2aWNlElcKDExpc3RQcm9maWxlcxIiLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBojLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USUQoKR2V0UHJvZmlsZRIgLmVuZ3JhbS5hcHAudjEuR2V0UHJvZmlsZVJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXNwb25zZRJaCg1DcmVhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlc3BvbnNlEloKDVVwZGF0ZVByb2ZpbGUSIy5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVzcG9uc2USbwoURGlzY292ZXJQcm9maWxlUmVwb3MSKi5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRJaCg1EZWxldGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5EZWxldGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlc3BvbnNlYgZwcm90bzM"); /** * An admin-curated bundle of session-launch inputs (ADR §1). @@ -394,6 +394,11 @@ export type ProfileIntegrationGrant = Message<"engram.app.v1.ProfileIntegrationG * @generated from field: string credential_scope = 4; */ credentialScope: string; + + /** + * @generated from field: engram.app.v1.McpToolGrant mcp = 5; + */ + mcp?: McpToolGrant; }; /** @@ -903,6 +908,38 @@ export type DeleteProfileResponse = Message<"engram.app.v1.DeleteProfileResponse export const DeleteProfileResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_profile, 21); +/** + * @generated from message engram.app.v1.McpToolGrant + */ +export type McpToolGrant = Message<"engram.app.v1.McpToolGrant"> & { + /** + * @generated from field: string upstream_name = 1; + */ + upstreamName: string; + + /** + * @generated from field: bool automation_write = 2; + */ + automationWrite: boolean; + + /** + * @generated from field: string argument_constraints_json = 3; + */ + argumentConstraintsJson: string; + + /** + * @generated from field: bool unrestricted = 4; + */ + unrestricted: boolean; +}; + +/** + * Describes the message engram.app.v1.McpToolGrant. + * Use `create(McpToolGrantSchema)` to create a new message. + */ +export const McpToolGrantSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_profile, 22); + /** * Session profiles (ADR 0053) — admin-curated session starting points. * ORCHESTRATOR-NATIVE: the control plane neither implements nor knows about diff --git a/orchestrator/src/integrations/grants.ts b/orchestrator/src/integrations/grants.ts index 6c4869b95..38e1f353b 100644 --- a/orchestrator/src/integrations/grants.ts +++ b/orchestrator/src/integrations/grants.ts @@ -1,5 +1,6 @@ /** Connection-aware integration grant validation and policy projection (ADR 0109). */ +import { validateMcpGrant } from "../mcp/types.ts"; import { ConnectError, Code } from "@connectrpc/connect"; import { createHash } from "node:crypto"; @@ -106,6 +107,15 @@ export async function resolveIntegrationGrants( Code.InvalidArgument, ); } + if (connection.provider === "mcp") { + try { + validateMcpGrant(grant); + } catch (error) { + throw new ConnectError((error as Error).message, Code.InvalidArgument); + } + } else if (grant.mcp) { + throw new ConnectError("MCP tool grants require an MCP connection", Code.InvalidArgument); + } if (!OPERATION_RE.test(grant.operation)) { throw new ConnectError( `invalid integration operation "${grant.operation}"`, @@ -141,6 +151,8 @@ export function grantsToCapabilities( ): string[] { const capabilities = new Set(); for (const { grant, connection } of resolved) { + // MCP authority cannot become a generic guest credential or egress grant. + if (connection.provider === "mcp") continue; if (grant.resourceConstraints.length === 0) { capabilities.add(`${connection.provider}:${grant.operation}`); continue; diff --git a/orchestrator/src/mcp/approvals.ts b/orchestrator/src/mcp/approvals.ts new file mode 100644 index 000000000..1536e34d9 --- /dev/null +++ b/orchestrator/src/mcp/approvals.ts @@ -0,0 +1,8 @@ +export function assertOwnerOrAdmin( + ownerId: string | null, + actorId: string, + isAdmin: boolean, +): void { + if (!actorId || (!isAdmin && (!ownerId || ownerId !== actorId))) + throw new Error("session owner or administrator required"); +} diff --git a/orchestrator/src/mcp/bindings.ts b/orchestrator/src/mcp/bindings.ts new file mode 100644 index 000000000..4d15f9ed7 --- /dev/null +++ b/orchestrator/src/mcp/bindings.ts @@ -0,0 +1,127 @@ +import type { ProfileIntegrationGrant } from "../db/schema.ts"; +import { validateMcpConfig } from "./config.ts"; +import { canonicalDigest, canonicalJson } from "./hash.ts"; +import { MCP_LIMITS } from "./limits.ts"; +import { + validateMcpGrant, + type McpBinding, + type McpConnectionState, + type McpReviewedTool, +} from "./types.ts"; + +export function compileBindings( + connections: readonly { + id: string; + alias: string; + config: unknown; + state: McpConnectionState; + enabled: boolean; + tools: readonly McpReviewedTool[]; + grants: readonly ProfileIntegrationGrant[]; + }[], +): McpBinding[] { + if (connections.length > MCP_LIMITS.connectionsPerSession) + throw new Error("too many MCP connections"); + const ids = new Set(); + const names = new Set(); + const bindings = connections.map((connection) => { + if (ids.has(connection.id)) throw new Error("duplicate MCP connection"); + ids.add(connection.id); + if ( + !connection.enabled || + connection.state.revoked || + !connection.state.credentialIdentity || + connection.state.catalogRevision < 1 + ) + throw new Error("MCP connection is unavailable"); + if (!/^[a-z][a-z0-9-]{1,62}$/.test(connection.alias)) + throw new Error("invalid connection alias"); + const granted = new Set(); + const tools = connection.grants.map((grant) => { + validateMcpGrant(grant); + if (grant.connectionId !== connection.id || granted.has(grant.mcp!.upstreamName)) + throw new Error("invalid or duplicate bound grant"); + granted.add(grant.mcp!.upstreamName); + const tool = connection.tools.find((t) => t.upstreamName === grant.mcp!.upstreamName); + if (!tool?.enabled) throw new Error("reviewed tool is unavailable"); + const name = `mcp_${connection.alias.replaceAll("-", "_").slice(0, 24)}_${canonicalDigest([connection.id, tool.upstreamName]).slice(7, 27)}`; + if (names.has(name)) throw new Error("tool name collision"); + names.add(name); + return { name, tool, grant: grant.mcp! }; + }); + const binding = { + connectionId: connection.id, + ...connection.state, + config: validateMcpConfig(connection.config), + tools, + policyHash: canonicalDigest(connection.grants), + }; + return structuredClone({ ...binding, hash: canonicalDigest(binding) }); + }); + if ( + names.size > MCP_LIMITS.toolsPerSession || + Buffer.byteLength(canonicalJson(bindings)) > MCP_LIMITS.manifestBytes + ) + throw new Error("MCP manifest exceeds session limits"); + return bindings; +} + +export function assertBindingCurrent( + binding: McpBinding, + live: McpConnectionState & { enabled: boolean }, +): void { + if ( + !live.enabled || + live.revoked || + binding.configRevision !== live.configRevision || + binding.authGeneration !== live.authGeneration || + binding.credentialIdentity !== live.credentialIdentity || + binding.catalogRevision !== live.catalogRevision + ) + throw new Error("MCP binding is stale; explicit rebind is required"); +} + +/** Child authority can only narrow the parent's effective tool grant. */ +export function intersectMcpGrants( + parent: readonly ProfileIntegrationGrant[], + requested: readonly ProfileIntegrationGrant[], +): ProfileIntegrationGrant[] { + return requested.map((child) => { + validateMcpGrant(child); + const inherited = parent.find( + (p) => + p.connectionId === child.connectionId && p.mcp?.upstreamName === child.mcp!.upstreamName, + ); + if (!inherited) throw new Error("child tool grant exceeds parent authority"); + validateMcpGrant(inherited); + const parentConstraint = inherited.mcp!.argumentConstraints; + const childConstraint = child.mcp!.argumentConstraints; + const unrestricted = inherited.mcp!.unrestricted && child.mcp!.unrestricted; + const argumentConstraints = unrestricted + ? {} + : inherited.mcp!.unrestricted + ? childConstraint + : child.mcp!.unrestricted + ? parentConstraint + : { allOf: [parentConstraint, childConstraint] }; + return structuredClone({ + ...child, + mcp: { + ...child.mcp!, + automationWrite: inherited.mcp!.automationWrite && child.mcp!.automationWrite, + unrestricted, + argumentConstraints, + }, + }); + }); +} + +/** Compare only authorization data, so profile display edits do not revoke tools. */ +export function assertBindingGrantsCurrent( + binding: McpBinding, + liveGrants: readonly ProfileIntegrationGrant[], +): void { + const grants = liveGrants.filter((grant) => grant.connectionId === binding.connectionId); + if (canonicalDigest(grants) !== binding.policyHash) + throw new Error("MCP grant policy changed; explicit rebind is required"); +} diff --git a/orchestrator/src/mcp/catalog.ts b/orchestrator/src/mcp/catalog.ts new file mode 100644 index 000000000..998252735 --- /dev/null +++ b/orchestrator/src/mcp/catalog.ts @@ -0,0 +1,110 @@ +import { canonicalDigest, canonicalJson } from "./hash.ts"; +import { MCP_LIMITS } from "./limits.ts"; +import type { McpReviewedTool } from "./types.ts"; + +/** Bound schema traversal before disposable-worker compilation. No remote resolution. */ +export function assertSchemaShape(schema: unknown): asserts schema is Record { + if (!schema || typeof schema !== "object" || Array.isArray(schema)) + throw new Error("schema must be an object"); + if (Buffer.byteLength(canonicalJson(schema)) > MCP_LIMITS.argumentBytes) + throw new Error("schema is too large"); + const schemaMaps = new Set([ + "properties", + "patternProperties", + "$defs", + "definitions", + "dependentSchemas", + ]); + const schemaLists = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]); + const schemaValues = new Set([ + "additionalProperties", + "unevaluatedProperties", + "propertyNames", + "contains", + "not", + "if", + "then", + "else", + "additionalItems", + "unevaluatedItems", + "contentSchema", + ]); + let nodes = 0; + function walk(v: unknown, depth: number, schemaNode: boolean): void { + if (++nodes > 10_000 || depth > 64) throw new Error("schema is too complex"); + if (!v || typeof v !== "object") return; + if (Array.isArray(v)) { + for (const child of v) walk(child, depth + 1, false); + return; + } + for (const [key, child] of Object.entries(v)) { + if ( + schemaNode && + ["$ref", "$dynamicRef", "$recursiveRef"].includes(key) && + (typeof child !== "string" || !child.startsWith("#")) + ) + throw new Error("schema references must be local"); + if ( + schemaNode && + key === "$schema" && + ![ + "http://json-schema.org/draft-07/schema#", + "https://json-schema.org/draft/2020-12/schema", + ].includes(String(child)) + ) + throw new Error("unsupported schema dialect"); + // Schema-map keys are property names. Annotation values are data. + if ( + schemaNode && + (schemaMaps.has(key) || key === "dependencies") && + child && + typeof child === "object" && + !Array.isArray(child) + ) { + for (const sub of Object.values(child)) walk(sub, depth + 2, !Array.isArray(sub)); + } else if (schemaNode && (schemaLists.has(key) || key === "items") && Array.isArray(child)) { + for (const sub of child) walk(sub, depth + 2, true); + } else walk(child, depth + 1, schemaNode && (schemaValues.has(key) || key === "items")); + } + } + walk(schema, 0, true); +} + +export function reviewCatalog(input: readonly unknown[]): McpReviewedTool[] { + if (input.length > MCP_LIMITS.toolsPerSession) throw new Error("too many tools"); + const names = new Set(); + const tools = input.map((raw) => { + if (!raw || typeof raw !== "object") throw new Error("invalid reviewed tool"); + const t = raw as Record; + if ( + typeof t.upstreamName !== "string" || + !t.upstreamName || + t.upstreamName.length > 256 || + /[\u0000-\u001f]/.test(t.upstreamName) || + names.has(t.upstreamName) + ) + throw new Error("invalid or duplicate tool name"); + names.add(t.upstreamName); + if ( + typeof t.description !== "string" || + t.description.length > 16_384 || + !["read", "write"].includes(String(t.classification)) || + typeof t.enabled !== "boolean" + ) + throw new Error("each tool needs explicit review"); + assertSchemaShape(t.inputSchema); + if (t.outputSchema !== undefined) assertSchemaShape(t.outputSchema); + const tool = { + upstreamName: t.upstreamName, + description: t.description, + inputSchema: t.inputSchema, + ...(t.outputSchema === undefined ? {} : { outputSchema: t.outputSchema }), + classification: t.classification as "read" | "write", + enabled: t.enabled, + }; + return structuredClone({ ...tool, fingerprint: canonicalDigest(tool) }); + }); + if (Buffer.byteLength(canonicalJson(tools)) > MCP_LIMITS.manifestBytes) + throw new Error("catalog is too large"); + return tools; +} diff --git a/orchestrator/src/mcp/config.ts b/orchestrator/src/mcp/config.ts new file mode 100644 index 000000000..7b61265a2 --- /dev/null +++ b/orchestrator/src/mcp/config.ts @@ -0,0 +1,45 @@ +import { isIP } from "node:net"; +import { z } from "zod"; + +// Naming convention only. Coordinator reservation metadata protects the sealed secret. +const reference = z.string().regex(/^mcp\.[a-zA-Z0-9_.-]{1,160}$/); +const configSchema = z + .object({ + endpoint: z.string().max(2048), + zone: z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), + auth: z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("api_key"), secretRef: reference }).strict(), + z + .object({ + kind: z.literal("oauth"), + clientRegistrationId: z.string().min(1).max(128), + shareWithOrgSessions: z.literal(true), + }) + .strict(), + ]), + }) + .strict(); + +export type McpConfig = z.infer; + +/** This validates stored shape. The worker also enforces operator DNS/zone policy. */ +export function validateMcpConfig(input: unknown): McpConfig { + const config = configSchema.parse(input); + const url = new URL(config.endpoint); + if ( + url.protocol !== "https:" || + url.username || + url.password || + url.search || + url.hash || + isIP(url.hostname.replace(/^\[|\]$/g, "")) !== 0 || + url.href !== config.endpoint || + url.hostname.length > 253 || + !url.hostname.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) + ) { + throw new Error( + "MCP endpoint must be a canonical HTTPS URL with a DNS hostname and no credentials, query, or fragment", + ); + } + return config; +} diff --git a/orchestrator/src/mcp/contracts.ts b/orchestrator/src/mcp/contracts.ts new file mode 100644 index 000000000..8f4155750 --- /dev/null +++ b/orchestrator/src/mcp/contracts.ts @@ -0,0 +1,334 @@ +import { Code, ConnectError, type ServiceImpl } from "@connectrpc/connect"; +import type { IntegrationService } from "../gen/engram/app/v1/integration_pb.ts"; +import type { + IntegrationConnectionRow, + IntegrationConnectionStore, +} from "../db/integration-connections.ts"; +import { requireAdmin, requireUser, type GetSession } from "../rpc/require.ts"; +import { assertOwnerOrAdmin } from "./approvals.ts"; +import { validateMcpConfig, type McpConfig } from "./config.ts"; +import type { McpRepository } from "./repository.ts"; +import type { McpJournal } from "./journal.ts"; +import type { McpDiscoveredTool, McpReviewedTool } from "./types.ts"; + +/** Internal services use trusted connection identities, never guest destinations. */ +export interface McpServices { + enabled: boolean; + repository: McpRepository; + journal: McpJournal; + sessionOwner?: (sessionId: string) => Promise; + validateDestination?: (config: McpConfig) => Promise; + validateSchemas?: (tools: readonly McpReviewedTool[]) => Promise; + discovery?: { discover(connection: IntegrationConnectionRow): Promise }; + authorization?: { + begin( + connection: IntegrationConnectionRow, + actorId: string, + ): Promise<{ authorizationUrl: string }>; + disconnect(connection: IntegrationConnectionRow): Promise; + }; + runtime?: { + cancel(executionId: string): Promise; + /** Must authorize the live owner/admin and acknowledge deny policy before activation. */ + rebind( + sessionId: string, + expectedBindingHash: string, + actorId: string, + isAdmin: boolean, + ): Promise<{ bindingHash: string }>; + }; +} +export function requireMcp(services: McpServices | undefined): McpServices { + if (!services?.enabled) + throw new ConnectError("external MCP is disabled", Code.FailedPrecondition); + return services; +} +export function requireMcpHook(hook: T | undefined): T { + if (!hook) + throw new ConnectError("external MCP service is not configured", Code.FailedPrecondition); + return hook; +} +export async function mcpOperation(run: () => Promise): Promise { + try { + return await run(); + } catch (error) { + if (error instanceof ConnectError) throw error; + const message = error instanceof Error ? error.message : "MCP operation failed"; + const code = /owner or administrator/.test(message) + ? Code.PermissionDenied + : /stale|already/.test(message) + ? Code.Aborted + : Code.FailedPrecondition; + // Internal worker errors can contain upstream metadata. Return a stable error. + throw new ConnectError( + code === Code.PermissionDenied + ? "session owner or administrator required" + : code === Code.Aborted + ? "MCP state changed; reload and try again" + : "MCP operation could not be completed", + code, + ); + } +} +export interface McpProtoConfig { + endpoint: string; + zone: string; + authKind: string; + apiKeySecretRef: string; + oauthClientRegistrationId: string; + shareWithOrgSessions: boolean; +} +export function mcpConfigFromProto(value: McpProtoConfig | undefined): McpConfig { + if (!value) throw new ConnectError("MCP config is required", Code.InvalidArgument); + if ( + (value.authKind === "api_key" && + (value.oauthClientRegistrationId || value.shareWithOrgSessions)) || + (value.authKind === "oauth" && value.apiKeySecretRef) + ) + throw new ConnectError("conflicting MCP credential configuration", Code.InvalidArgument); + try { + return validateMcpConfig({ + endpoint: value.endpoint, + zone: value.zone, + auth: + value.authKind === "api_key" + ? { kind: value.authKind, secretRef: value.apiKeySecretRef } + : { + kind: value.authKind, + clientRegistrationId: value.oauthClientRegistrationId, + shareWithOrgSessions: value.shareWithOrgSessions, + }, + }); + } catch { + throw new ConnectError("invalid MCP configuration", Code.InvalidArgument); + } +} +export function mcpConfigToProto(raw: unknown): McpProtoConfig { + const value = validateMcpConfig(raw); + return { + endpoint: value.endpoint, + zone: value.zone, + authKind: value.auth.kind, + apiKeySecretRef: value.auth.kind === "api_key" ? value.auth.secretRef : "", + oauthClientRegistrationId: value.auth.kind === "oauth" ? value.auth.clientRegistrationId : "", + shareWithOrgSessions: value.auth.kind === "oauth" && value.auth.shareWithOrgSessions, + }; +} +function catalogToProto(value: Awaited>) { + return { + ...value, + tools: value.tools.map((t) => ({ + upstreamName: t.upstreamName, + description: t.description, + inputSchemaJson: JSON.stringify(t.inputSchema), + outputSchemaJson: t.outputSchema ? JSON.stringify(t.outputSchema) : "", + classification: "classification" in t ? String(t.classification) : "", + enabled: "enabled" in t && t.enabled === true, + fingerprint: "fingerprint" in t ? String(t.fingerprint) : "", + })), + }; +} +function parseSchema(value: string): Record { + try { + const schema: unknown = JSON.parse(value); + if (!schema || typeof schema !== "object" || Array.isArray(schema)) throw new Error(); + return schema as Record; + } catch { + throw new ConnectError("schema must be a JSON object", Code.InvalidArgument); + } +} +type Methods = + | "discoverMcp" + | "publishMcp" + | "authorizeMcp" + | "disconnectMcp" + | "listMcpApprovals" + | "decideMcpApproval" + | "cancelMcpCall" + | "resolveMcpOutcome" + | "rebindMcp"; +export function mcpHandlers(deps: { + services?: McpServices; + connections: IntegrationConnectionStore; + getSession: GetSession; + now: () => Date; + connectionToProto: (row: IntegrationConnectionRow) => { id: string }; +}): Pick, Methods> { + const services = () => requireMcp(deps.services); + async function connection(id: string) { + const row = await deps.connections.get(id); + if (!row || row.provider !== "mcp" || !row.mcp) + throw new ConnectError("MCP connection not found", Code.NotFound); + return row; + } + return { + async discoverMcp(req, ctx) { + const actor = await requireAdmin(ctx, deps.getSession); + const service = services(); + const discover = requireMcpHook(service.discovery); + return mcpOperation(async () => { + const row = await connection(req.connectionId); + if (row.mcp!.configRevision !== req.expectedRevision) throw new Error("stale connection"); + const tools = await discover.discover(row); + return { + catalog: catalogToProto( + await service.repository.saveDraft(row.id, row.mcp!, tools, actor.id, deps.now()), + ), + }; + }); + }, + async publishMcp(req, ctx) { + const actor = await requireAdmin(ctx, deps.getSession); + const service = services(); + const validate = requireMcpHook(service.validateSchemas); + const { reviewCatalog } = await import("./catalog.ts"); + let tools: McpReviewedTool[]; + try { + tools = reviewCatalog( + req.tools.map((t) => ({ + upstreamName: t.upstreamName, + description: t.description, + inputSchema: parseSchema(t.inputSchemaJson), + ...(t.outputSchemaJson ? { outputSchema: parseSchema(t.outputSchemaJson) } : {}), + classification: t.classification, + enabled: t.enabled, + })), + ); + } catch { + throw new ConnectError("invalid reviewed tools", Code.InvalidArgument); + } + return mcpOperation(async () => { + await validate(tools); + return { + catalog: catalogToProto( + await service.repository.publish( + req.connectionId, + req.draftId, + req.expectedRevision, + tools, + actor.id, + deps.now(), + ), + ), + }; + }); + }, + async authorizeMcp(req, ctx) { + const actor = await requireAdmin(ctx, deps.getSession); + const service = services(); + const authorization = requireMcpHook(service.authorization); + return mcpOperation(async () => { + const row = await connection(req.connectionId); + const config = validateMcpConfig(row.config); + if (!req.shareWithOrgSessions || config.auth.kind !== "oauth") + throw new ConnectError( + "explicit consent to organization session sharing is required", + Code.InvalidArgument, + ); + if (row.mcp!.authGeneration !== req.expectedGeneration) + throw new Error("stale authorization"); + return authorization.begin(row, actor.id); + }); + }, + async disconnectMcp(req, ctx) { + await requireAdmin(ctx, deps.getSession); + const service = services(); + const authorization = requireMcpHook(service.authorization); + return mcpOperation(async () => { + const row = await connection(req.connectionId); + // Revoke local authority before external cleanup, which can fail. + await service.repository.disconnect(row.id, req.expectedGeneration, deps.now()); + await authorization.disconnect(row); + return { connection: deps.connectionToProto(await connection(row.id)) }; + }); + }, + async listMcpApprovals(req, ctx) { + const actor = await requireUser(ctx, deps.getSession); + const service = services(); + return mcpOperation(async () => ({ + approvals: ( + await service.journal.pending(req.sessionId, actor.id, actor.role === "admin", deps.now()) + ).map(({ call, approval }) => ({ + executionId: call.id, + sessionId: call.sessionId, + runId: call.runId, + toolCallId: call.toolCallId, + connectionId: call.connectionId, + upstreamName: call.upstreamName, + requestDigest: call.requestDigest, + argumentHash: call.argumentHash, + expiresAt: approval.expiresAt.toISOString(), + })), + })); + }, + async decideMcpApproval(req, ctx) { + const actor = await requireUser(ctx, deps.getSession); + const service = services(); + if (actor.serviceAccount) + throw new ConnectError("a human approval is required", Code.PermissionDenied); + return mcpOperation(async () => ({ + state: ( + await service.journal.decide( + req.executionId, + actor.id, + actor.role === "admin", + req.approve, + deps.now(), + ) + ).state, + })); + }, + async cancelMcpCall(req, ctx) { + const actor = await requireUser(ctx, deps.getSession); + const service = services(); + const runtime = requireMcpHook(service.runtime); + return mcpOperation(async () => { + const call = await service.journal.cancel( + req.executionId, + actor.id, + actor.role === "admin", + deps.now(), + ); + await runtime.cancel(call.id); + return { state: call.state }; + }); + }, + async resolveMcpOutcome(req, ctx) { + const actor = await requireUser(ctx, deps.getSession); + const service = services(); + if (actor.serviceAccount) + throw new ConnectError("a human decision is required", Code.PermissionDenied); + if (req.outcome !== "succeeded" && req.outcome !== "failed" && req.outcome !== "acknowledged") + throw new ConnectError("invalid outcome resolution", Code.InvalidArgument); + const outcome = req.outcome; + return mcpOperation(async () => ({ + outcome: ( + await service.journal.resolveUnknown( + req.executionId, + actor.id, + actor.role === "admin", + outcome, + deps.now(), + ) + ).outcome, + })); + }, + async rebindMcp(req, ctx) { + const actor = await requireUser(ctx, deps.getSession); + const service = services(); + const runtime = requireMcpHook(service.runtime); + if (!req.expectedBindingHash) + throw new ConnectError("expected binding hash is required", Code.InvalidArgument); + const owner = requireMcpHook(service.sessionOwner); + return mcpOperation(async () => { + assertOwnerOrAdmin(await owner(req.sessionId), actor.id, actor.role === "admin"); + return runtime.rebind( + req.sessionId, + req.expectedBindingHash, + actor.id, + actor.role === "admin", + ); + }); + }, + }; +} diff --git a/orchestrator/src/mcp/hash.ts b/orchestrator/src/mcp/hash.ts new file mode 100644 index 000000000..4cffea471 --- /dev/null +++ b/orchestrator/src/mcp/hash.ts @@ -0,0 +1,31 @@ +import { createHash } from "node:crypto"; + +/** Strict JSON canonicalization: no silent removal of unsupported values. */ +export function canonicalJson(value: unknown): string { + const seen = new Set(); + function visit(v: unknown): unknown { + if (v === null || typeof v === "string" || typeof v === "boolean") return v; + if (typeof v === "number" && Number.isFinite(v)) return v; + if (typeof v !== "object" || v === null || seen.has(v)) + throw new Error("expected finite, acyclic JSON"); + seen.add(v); + let result: unknown; + if (Array.isArray(v)) result = v.map(visit); + else { + if (Object.getPrototypeOf(v) !== Object.prototype && Object.getPrototypeOf(v) !== null) + throw new Error("expected plain JSON object"); + result = Object.fromEntries( + Object.keys(v) + .sort() + .map((k) => [k, visit((v as Record)[k])]), + ); + } + seen.delete(v); + return result; + } + return JSON.stringify(visit(value)); +} + +export function canonicalDigest(value: unknown): string { + return `sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`; +} diff --git a/orchestrator/src/mcp/journal.ts b/orchestrator/src/mcp/journal.ts new file mode 100644 index 000000000..74c23a8c1 --- /dev/null +++ b/orchestrator/src/mcp/journal.ts @@ -0,0 +1,560 @@ +import { and, asc, eq, gt, inArray, isNull, lte, sql } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { + integrationConnection as connection, + mcpCatalog as catalog, + mcpExecution as execution, + mcpApproval as approval, + mcpResolution as resolution, + mcpAdmission as admission, + mcpAdmissionScope as scope, + task, +} from "../db/schema.ts"; +import { assertOwnerOrAdmin } from "./approvals.ts"; +import { canonicalDigest, canonicalJson } from "./hash.ts"; +import { MCP_LIMITS } from "./limits.ts"; +import { validateMcpGrant, type McpToolGrant, type McpExecutionState } from "./types.ts"; + +type Db = ReturnType; +type Tx = Parameters[0]>[0]; +export type McpCallAuthorization = + | { kind: "read" } + | { kind: "approval" } + | { kind: "automation"; grant: McpToolGrant }; +export type McpCallInput = Pick< + typeof execution.$inferInsert, + | "taskId" + | "sessionId" + | "runId" + | "toolCallId" + | "connectionId" + | "catalogId" + | "principalId" + | "bindingHash" + | "argumentHash" + | "upstreamName" + | "configRevision" + | "authGeneration" + | "classification" + | "deadline" +>; + +async function lockScope(tx: Tx, id: string) { + await tx.insert(scope).values({ id }).onConflictDoNothing(); + await tx.select().from(scope).where(eq(scope.id, id)).for("update"); +} +async function owned( + tx: Tx, + call: typeof execution.$inferSelect, + actorId: string, + isAdmin: boolean, +) { + const [owner] = await tx + .select({ id: task.createdByUserId }) + .from(task) + .where(eq(task.id, call.taskId)); + assertOwnerOrAdmin(owner?.id ?? null, actorId, isAdmin); +} +async function unknown(tx: Tx | Db, sessionId: string, connectionId: string): Promise { + const rows = await tx + .select({ id: execution.id }) + .from(execution) + .leftJoin(resolution, eq(resolution.executionId, execution.id)) + .where( + and( + eq(execution.sessionId, sessionId), + eq(execution.connectionId, connectionId), + eq(execution.classification, "write"), + eq(execution.state, "outcome_unknown"), + isNull(resolution.executionId), + ), + ) + .limit(1); + return rows.length > 0; +} + +export function makeMcpJournal(db = getDb()) { + return { + async get(id: string) { + return (await db.select().from(execution).where(eq(execution.id, id)))[0] ?? null; + }, + hasUnknown(sessionId: string, connectionId: string) { + return unknown(db, sessionId, connectionId); + }, + async create(input: McpCallInput, authorization: McpCallAuthorization, now: Date) { + input = structuredClone(input); + authorization = structuredClone(authorization); + if ( + ![input.argumentHash, input.bindingHash].every((hash) => /^sha256:[0-9a-f]{64}$/.test(hash)) + ) + throw new Error("canonical binding and argument hashes are required"); + if ( + input.deadline <= now || + input.deadline.getTime() - now.getTime() > + MCP_LIMITS.approvalMs + MCP_LIMITS.queueMs + MCP_LIMITS.maxToolMs || + !input.runId || + !input.toolCallId || + !input.principalId + ) + throw new Error("invalid call identity or deadline"); + if (authorization.kind === "read" && input.classification !== "read") + throw new Error("write requires approval or an explicit automation grant"); + if (authorization.kind === "automation") { + validateMcpGrant({ + connectionId: input.connectionId, + operation: "invoke", + resourceConstraints: [], + credentialScope: "org", + mcp: authorization.grant, + }); + if ( + input.classification !== "write" || + !authorization.grant.automationWrite || + authorization.grant.upstreamName !== input.upstreamName + ) + throw new Error("explicit automation write grant is required"); + } + const state = authorization.kind === "approval" ? "waiting_approval" : "authorized"; + const authorizationKind = authorization.kind; + const automationGrantHash = + authorization.kind === "automation" ? canonicalDigest(authorization.grant) : null; + const requestDigest = canonicalDigest({ + ...input, + deadline: input.deadline.toISOString(), + authorizationKind, + automationGrantHash, + }); + return db.transaction(async (tx) => { + await tx + .insert(execution) + .values({ + ...input, + id: crypto.randomUUID(), + state, + authorizationKind, + automationGrantHash, + requestDigest, + createdAt: now, + updatedAt: now, + }) + .onConflictDoNothing({ target: [execution.sessionId, execution.toolCallId] }); + const [row] = await tx + .select() + .from(execution) + .where( + and( + eq(execution.sessionId, input.sessionId), + eq(execution.toolCallId, input.toolCallId), + ), + ); + if (!row || row.requestDigest !== requestDigest) throw new Error("call digest mismatch"); + return row; + }); + }, + async requestApproval(id: string, deploymentId: string, now: Date) { + return db.transaction(async (tx) => { + await lockScope(tx, deploymentId); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + if (!call || call.state !== "waiting_approval" || call.deadline <= now) + throw new Error("call is not pending approval"); + const [existing] = await tx.select().from(approval).where(eq(approval.executionId, id)); + if (existing) return existing; + const pending = await tx + .select() + .from(approval) + .where( + and( + eq(approval.deploymentId, deploymentId), + eq(approval.decision, "pending"), + gt(approval.expiresAt, now), + ), + ); + if ( + pending.length >= MCP_LIMITS.deploymentApprovals || + pending.filter((a) => a.sessionId === call.sessionId).length >= + MCP_LIMITS.sessionApprovals + ) + throw new Error("approval capacity reached"); + const [row] = await tx + .insert(approval) + .values({ + executionId: id, + deploymentId, + sessionId: call.sessionId, + requestDigest: call.requestDigest, + decision: "pending", + expiresAt: new Date( + Math.min(call.deadline.getTime(), now.getTime() + MCP_LIMITS.approvalMs), + ), + }) + .returning(); + return row!; + }); + }, + async pending(sessionId: string, actorId: string, isAdmin: boolean, now: Date) { + return db.transaction(async (tx) => { + const rows = await tx + .select({ call: execution, approval }) + .from(execution) + .innerJoin(approval, eq(approval.executionId, execution.id)) + .where( + and( + eq(execution.sessionId, sessionId), + eq(approval.decision, "pending"), + gt(approval.expiresAt, now), + ), + ); + for (const row of rows) await owned(tx, row.call, actorId, isAdmin); + return rows; + }); + }, + async decide(id: string, actorId: string, isAdmin: boolean, approve: boolean, now: Date) { + return db.transaction(async (tx) => { + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + if (!call) throw new Error("call not found"); + await owned(tx, call, actorId, isAdmin); + if (call.state !== "waiting_approval") throw new Error("approval is no longer pending"); + const rows = await tx + .update(approval) + .set({ decision: approve ? "approved" : "denied", actorId, decidedAt: now }) + .where( + and( + eq(approval.executionId, id), + eq(approval.requestDigest, call.requestDigest), + eq(approval.decision, "pending"), + gt(approval.expiresAt, now), + ), + ) + .returning(); + if (!rows.length) throw new Error("approval expired or already decided"); + const [updated] = await tx + .update(execution) + .set({ + state: approve ? "authorized" : "canceled", + version: call.version + 1, + updatedAt: now, + }) + .where(eq(execution.id, id)) + .returning(); + return updated!; + }); + }, + async admit( + id: string, + deploymentId: string, + now: Date, + toolMs = MCP_LIMITS.toolMs, + ): Promise<"queued" | "admitted"> { + if (!Number.isSafeInteger(toolMs) || toolMs < 1 || toolMs > MCP_LIMITS.maxToolMs) + throw new Error("invalid tool deadline"); + return db.transaction(async (tx) => { + await lockScope(tx, deploymentId); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + if (!call || call.state !== "authorized" || call.deadline <= now) + throw new Error("call is not ready for admission"); + const rows = await tx + .select() + .from(admission) + .where(eq(admission.deploymentId, deploymentId)) + .orderBy(asc(admission.createdAt)); + const existing = rows.find((a) => a.executionId === id); + if (existing?.state === "admitted") return "admitted"; + if (existing && existing.expiresAt <= now) throw new Error("queue entry expired"); + // Expired active leases still count until expire() records the unknown outcome. + const active = rows.filter((a) => a.state === "admitted"); + const available = + active.length < MCP_LIMITS.deploymentConcurrency && + active.filter((a) => a.connectionId === call.connectionId).length < + MCP_LIMITS.connectionConcurrency && + active.filter((a) => a.sessionId === call.sessionId).length < + MCP_LIMITS.sessionConcurrency; + const state = available ? "admitted" : "queued"; + if ( + !available && + !existing && + rows.filter((a) => a.state === "queued").length >= MCP_LIMITS.queueSize + ) + throw new Error("MCP queue is full"); + const expiresAt = new Date( + Math.min( + call.deadline.getTime(), + now.getTime() + (available ? toolMs : MCP_LIMITS.queueMs), + ), + ); + if (existing) + await tx.update(admission).set({ state, expiresAt }).where(eq(admission.executionId, id)); + else + await tx.insert(admission).values({ + executionId: id, + deploymentId, + sessionId: call.sessionId, + connectionId: call.connectionId, + state, + expiresAt, + createdAt: now, + }); + return state; + }); + }, + /** Irreversible dispatch barrier. The upstream effect runs after this commits. */ + async dispatch( + id: string, + intent: { bootId: string; intentId: string; expiresAt: Date }, + now: Date, + ) { + return db.transaction(async (tx) => { + // Connection lock serializes unknown-outcome checks with finish/revoke. + const [identity] = await tx + .select({ connectionId: execution.connectionId }) + .from(execution) + .where(eq(execution.id, id)); + if (!identity) throw new Error("call not found"); + const [live] = await tx + .select() + .from(connection) + .where(eq(connection.id, identity.connectionId)) + .for("update"); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + if (!call || call.state !== "authorized" || call.deadline <= now) + throw new Error("call cannot dispatch"); + const [review] = await tx.select().from(catalog).where(eq(catalog.id, call.catalogId)); + if ( + !live || + live.provider !== "mcp" || + !live.enabled || + live.revoked || + live.configRevision !== call.configRevision || + live.authGeneration !== call.authGeneration || + !review || + review.connectionId !== live.id || + review.kind !== "published" || + review.revision !== live.catalogRevision || + review.configRevision !== call.configRevision || + review.authGeneration !== call.authGeneration || + review.credentialIdentity !== live.credentialIdentity + ) + throw new Error("stale MCP authority"); + const reviewedTool = review.tools.find((t) => t.upstreamName === call.upstreamName); + if ( + !reviewedTool || + !("classification" in reviewedTool) || + reviewedTool.classification !== call.classification || + !("enabled" in reviewedTool) || + !reviewedTool.enabled + ) + throw new Error("tool is not approved for this operation"); + if ( + call.classification === "write" && + (await unknown(tx, call.sessionId, call.connectionId)) + ) + throw new Error("unknown write outcome blocks this connection"); + const [lease] = await tx + .select() + .from(admission) + .where( + and( + eq(admission.executionId, id), + eq(admission.state, "admitted"), + gt(admission.expiresAt, now), + ), + ); + if ( + !lease || + !intent.bootId || + !intent.intentId || + intent.expiresAt <= now || + intent.expiresAt > lease.expiresAt + ) + throw new Error("valid admission and intent are required"); + const [decision] = await tx.select().from(approval).where(eq(approval.executionId, id)); + if (call.authorizationKind === "approval" && !decision) + throw new Error("approval is required"); + if (decision) { + const consumed = await tx + .update(approval) + .set({ consumedAt: now }) + .where( + and( + eq(approval.executionId, id), + eq(approval.requestDigest, call.requestDigest), + eq(approval.decision, "approved"), + isNull(approval.consumedAt), + gt(approval.expiresAt, now), + ), + ) + .returning(); + if (!consumed.length) throw new Error("approval expired or consumed"); + } + const [updated] = await tx + .update(execution) + .set({ + state: "dispatching", + version: call.version + 1, + bootId: intent.bootId, + intentId: intent.intentId, + intentExpiresAt: intent.expiresAt, + updatedAt: now, + }) + .where(eq(execution.id, id)) + .returning(); + return updated!; + }); + }, + async finish( + id: string, + version: number, + state: "succeeded" | "failed" | "outcome_unknown", + result: Record | null, + resultUnavailable: boolean, + now: Date, + ) { + if (result && Buffer.byteLength(canonicalJson(result)) > MCP_LIMITS.resultBytes) + throw new Error("result exceeds limit"); + if (state === "succeeded" && result === null && !resultUnavailable) + throw new Error("success needs a verified result or an explicit unavailable result"); + if (state !== "succeeded" && resultUnavailable) + throw new Error("unavailable result requires known success"); + return db.transaction(async (tx) => { + const [identity] = await tx + .select({ connectionId: execution.connectionId }) + .from(execution) + .where(eq(execution.id, id)); + if (!identity) throw new Error("call not found"); + await tx + .select() + .from(connection) + .where(eq(connection.id, identity.connectionId)) + .for("update"); + const [row] = await tx + .update(execution) + .set({ state, result, resultUnavailable, version: version + 1, updatedAt: now }) + .where( + and( + eq(execution.id, id), + eq(execution.version, version), + state === "outcome_unknown" + ? eq(execution.state, "dispatching") + : inArray(execution.state, ["dispatching", "outcome_unknown"]), + ), + ) + .returning(); + if (!row) throw new Error("stale execution transition"); + await tx.delete(admission).where(eq(admission.executionId, id)); + return row; + }); + }, + async cancel(id: string, actorId: string, isAdmin: boolean, now: Date) { + return db.transaction(async (tx) => { + const [identity] = await tx + .select({ connectionId: execution.connectionId }) + .from(execution) + .where(eq(execution.id, id)); + if (!identity) throw new Error("call not found"); + await tx + .select() + .from(connection) + .where(eq(connection.id, identity.connectionId)) + .for("update"); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + await owned(tx, call!, actorId, isAdmin); + if (!["waiting_approval", "authorized", "dispatching"].includes(call!.state)) return call!; + const state: McpExecutionState = + call!.state === "dispatching" ? "outcome_unknown" : "canceled"; + const [updated] = await tx + .update(execution) + .set({ state, version: call!.version + 1, updatedAt: now }) + .where(eq(execution.id, id)) + .returning(); + await tx.delete(admission).where(eq(admission.executionId, id)); + await tx + .update(approval) + .set({ decision: "denied", actorId, decidedAt: now }) + .where(and(eq(approval.executionId, id), eq(approval.decision, "pending"))); + return updated!; + }); + }, + async resolveUnknown( + id: string, + actorId: string, + isAdmin: boolean, + outcome: "succeeded" | "failed" | "acknowledged", + now: Date, + ) { + return db.transaction(async (tx) => { + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + if (!call || call.state !== "outcome_unknown") + throw new Error("call has no unknown outcome"); + await owned(tx, call, actorId, isAdmin); + const [row] = await tx + .insert(resolution) + .values({ executionId: id, actorId, outcome, createdAt: now }) + .onConflictDoNothing() + .returning(); + if (!row) throw new Error("outcome already resolved"); + // Human resolution unblocks the connection but never invents an upstream result. + return row; + }); + }, + async expire(deploymentId: string, now: Date) { + // Lock order is scope -> connection -> execution, shared with admission/dispatch. + return db.transaction(async (tx) => { + await lockScope(tx, deploymentId); + const expired = await tx + .select() + .from(admission) + .where(and(eq(admission.deploymentId, deploymentId), lte(admission.expiresAt, now))); + for (const lease of expired.sort((a, b) => a.connectionId.localeCompare(b.connectionId))) { + await tx + .select() + .from(connection) + .where(eq(connection.id, lease.connectionId)) + .for("update"); + const [call] = await tx + .select() + .from(execution) + .where(eq(execution.id, lease.executionId)) + .for("update"); + if (call && ["authorized", "dispatching"].includes(call.state)) + await tx + .update(execution) + .set({ + state: call.state === "dispatching" ? "outcome_unknown" : "canceled", + version: call.version + 1, + updatedAt: now, + }) + .where(eq(execution.id, call.id)); + await tx.delete(admission).where(eq(admission.executionId, lease.executionId)); + } + const pending = await tx + .select() + .from(approval) + .where( + and( + eq(approval.deploymentId, deploymentId), + inArray(approval.decision, ["pending", "approved"]), + isNull(approval.consumedAt), + lte(approval.expiresAt, now), + ), + ); + for (const item of pending) { + await tx.select().from(execution).where(eq(execution.id, item.executionId)).for("update"); + await tx + .update(approval) + .set({ decision: "expired", decidedAt: now }) + .where(and(eq(approval.executionId, item.executionId), isNull(approval.consumedAt))); + await tx + .update(execution) + .set({ state: "canceled", version: sql`${execution.version} + 1`, updatedAt: now }) + .where( + and( + eq(execution.id, item.executionId), + inArray(execution.state, ["waiting_approval", "authorized"]), + ), + ); + } + return expired.length + pending.length; + }); + }, + }; +} +export type McpJournal = ReturnType; diff --git a/orchestrator/src/mcp/limits.ts b/orchestrator/src/mcp/limits.ts new file mode 100644 index 000000000..7617f0c3f --- /dev/null +++ b/orchestrator/src/mcp/limits.ts @@ -0,0 +1,22 @@ +/** Operator ceilings. Per-zone policy can reduce these values. */ +export const MCP_LIMITS = Object.freeze({ + connectionsPerSession: 16, + toolsPerSession: 128, + manifestBytes: 1024 * 1024, + argumentBytes: 256 * 1024, + resultBytes: 1024 * 1024, + responseBytes: 2 * 1024 * 1024, + connectMs: 5000, + toolMs: 60_000, + maxToolMs: 300_000, + sessionConcurrency: 4, + connectionConcurrency: 8, + deploymentConcurrency: 32, + queueSize: 128, + queueMs: 30_000, + sessionApprovals: 16, + deploymentApprovals: 128, + approvalMs: 600_000, + schemaMs: 1000, + schemaHeapMiB: 128, +}); diff --git a/orchestrator/src/mcp/repository.ts b/orchestrator/src/mcp/repository.ts new file mode 100644 index 000000000..65b15ff80 --- /dev/null +++ b/orchestrator/src/mcp/repository.ts @@ -0,0 +1,293 @@ +import { and, desc, eq, sql } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { integrationConnection as connection, mcpCatalog as catalog } from "../db/schema.ts"; +import { assertSchemaShape, reviewCatalog } from "./catalog.ts"; +import { validateMcpConfig } from "./config.ts"; +import { canonicalDigest, canonicalJson } from "./hash.ts"; +import { MCP_LIMITS } from "./limits.ts"; +import type { McpConnectionState, McpDiscoveredTool } from "./types.ts"; + +export function makeMcpRepository(db = getDb()) { + return { + async updateConfig( + id: string, + expectedRevision: number, + input: { alias: string; displayName: string; config: unknown }, + now: Date, + ) { + const config = validateMcpConfig(input.config); + const rows = await db + .update(connection) + .set({ + alias: input.alias, + displayName: input.displayName, + config, + configRevision: sql`${connection.configRevision} + 1`, + enabled: false, + testedAt: null, + updatedAt: now, + }) + .where( + and( + eq(connection.id, id), + eq(connection.provider, "mcp"), + eq(connection.configRevision, expectedRevision), + ), + ) + .returning(); + if (!rows[0]) throw new Error("stale connection revision"); + return rows[0]; + }, + /** Only a verified credential exchange may call this method. Refresh does not call it. */ + async setAuthorization( + id: string, + expectedConfigRevision: number, + expectedGeneration: number, + credentialIdentity: string, + now: Date, + ) { + if (!credentialIdentity || credentialIdentity.length > 512) + throw new Error("credential identity is required"); + const rows = await db + .update(connection) + .set({ + credentialIdentity, + authGeneration: sql`${connection.authGeneration} + 1`, + revoked: false, + enabled: false, + testedAt: null, + updatedAt: now, + }) + .where( + and( + eq(connection.id, id), + eq(connection.provider, "mcp"), + eq(connection.authGeneration, expectedGeneration), + eq(connection.configRevision, expectedConfigRevision), + ), + ) + .returning(); + if (!rows[0]) throw new Error("stale authorization generation"); + return rows[0]; + }, + async disconnect(id: string, expectedGeneration: number, now: Date) { + const rows = await db + .update(connection) + .set({ + revoked: true, + enabled: false, + testedAt: null, + authGeneration: sql`${connection.authGeneration} + 1`, + updatedAt: now, + }) + .where( + and( + eq(connection.id, id), + eq(connection.provider, "mcp"), + eq(connection.authGeneration, expectedGeneration), + ), + ) + .returning(); + if (!rows[0]) throw new Error("stale authorization generation"); + return rows[0]; + }, + async setEnabled(id: string, expectedRevision: number, enabled: boolean, now: Date) { + return db.transaction(async (tx) => { + const [row] = await tx + .select() + .from(connection) + .where(and(eq(connection.id, id), eq(connection.provider, "mcp"))) + .for("update"); + if (!row || row.configRevision !== expectedRevision) + throw new Error("stale connection revision"); + if (enabled) { + const [active] = await tx + .select() + .from(catalog) + .where( + and( + eq(catalog.connectionId, id), + eq(catalog.kind, "published"), + eq(catalog.revision, row.catalogRevision), + ), + ); + if ( + row.revoked || + !row.credentialIdentity || + !active || + active.configRevision !== row.configRevision || + active.authGeneration !== row.authGeneration || + active.credentialIdentity !== row.credentialIdentity + ) + throw new Error("current reviewed catalog and authorization required"); + } + const [updated] = await tx + .update(connection) + .set({ enabled, updatedAt: now }) + .where(eq(connection.id, id)) + .returning(); + return updated!; + }); + }, + async saveDraft( + id: string, + expected: McpConnectionState, + tools: McpDiscoveredTool[], + actorId: string, + now: Date, + ) { + expected = structuredClone(expected); + tools = structuredClone(tools); + if ( + tools.length > MCP_LIMITS.toolsPerSession || + Buffer.byteLength(canonicalJson(tools)) > MCP_LIMITS.manifestBytes + ) + throw new Error("catalog is too large"); + const names = new Set(); + for (const t of tools) { + if ( + !t.upstreamName || + t.upstreamName.length > 256 || + names.has(t.upstreamName) || + typeof t.description !== "string" + ) + throw new Error("invalid discovery"); + names.add(t.upstreamName); + assertSchemaShape(t.inputSchema); + if (t.outputSchema !== undefined) assertSchemaShape(t.outputSchema); + } + // Pick only untrusted metadata. Discovery never grants read or write authority. + const discovered = tools.map((t) => ({ + upstreamName: t.upstreamName, + description: t.description, + inputSchema: t.inputSchema, + ...(t.outputSchema === undefined ? {} : { outputSchema: t.outputSchema }), + })); + return db.transaction(async (tx) => { + const [row] = await tx + .select() + .from(connection) + .where(and(eq(connection.id, id), eq(connection.provider, "mcp"))) + .for("update"); + if ( + !row || + row.revoked || + row.configRevision !== expected.configRevision || + row.authGeneration !== expected.authGeneration || + row.credentialIdentity !== expected.credentialIdentity + ) + throw new Error("stale discovery"); + const [draft] = await tx + .insert(catalog) + .values({ + id: crypto.randomUUID(), + connectionId: id, + kind: "draft", + revision: 0, + configRevision: row.configRevision, + authGeneration: row.authGeneration, + credentialIdentity: row.credentialIdentity, + tools: discovered, + fingerprint: canonicalDigest(discovered), + actorId, + createdAt: now, + }) + .returning(); + return draft!; + }); + }, + async publish( + id: string, + draftId: string, + expectedRevision: number, + input: readonly unknown[], + actorId: string, + now: Date, + ) { + const tools = reviewCatalog(input); + return db.transaction(async (tx) => { + const [row] = await tx + .select() + .from(connection) + .where(and(eq(connection.id, id), eq(connection.provider, "mcp"))) + .for("update"); + const [draft] = await tx + .select() + .from(catalog) + .where( + and(eq(catalog.id, draftId), eq(catalog.connectionId, id), eq(catalog.kind, "draft")), + ); + if ( + !row || + !draft || + row.revoked || + !row.credentialIdentity || + row.catalogRevision !== expectedRevision || + draft.configRevision !== row.configRevision || + draft.authGeneration !== row.authGeneration || + draft.credentialIdentity !== row.credentialIdentity + ) + throw new Error("stale catalog revision or discovery"); + const originals = new Map(draft.tools.map((t) => [t.upstreamName, t])); + for (const t of tools) { + const original = originals.get(t.upstreamName); + const metadata = { + upstreamName: t.upstreamName, + description: t.description, + inputSchema: t.inputSchema, + ...(t.outputSchema === undefined ? {} : { outputSchema: t.outputSchema }), + }; + if (!original || canonicalDigest(original) !== canonicalDigest(metadata)) + throw new Error("review cannot change discovered metadata"); + } + const revision = expectedRevision + 1; + const [published] = await tx + .insert(catalog) + .values({ + id: crypto.randomUUID(), + connectionId: id, + kind: "published", + revision, + configRevision: row.configRevision, + authGeneration: row.authGeneration, + credentialIdentity: row.credentialIdentity, + tools, + fingerprint: canonicalDigest(tools), + actorId, + createdAt: now, + }) + .returning(); + await tx + .update(connection) + .set({ catalogRevision: revision, enabled: false, testedAt: now, updatedAt: now }) + .where(eq(connection.id, id)); + return published!; + }); + }, + async getCatalog(id: string, revision: number) { + return ( + ( + await db + .select() + .from(catalog) + .where( + and( + eq(catalog.connectionId, id), + eq(catalog.kind, "published"), + eq(catalog.revision, revision), + ), + ) + )[0] ?? null + ); + }, + async listCatalogs(id: string) { + return db + .select() + .from(catalog) + .where(eq(catalog.connectionId, id)) + .orderBy(desc(catalog.createdAt)) + .limit(100); + }, + }; +} +export type McpRepository = ReturnType; diff --git a/orchestrator/src/mcp/types.ts b/orchestrator/src/mcp/types.ts new file mode 100644 index 000000000..af8a3f9a1 --- /dev/null +++ b/orchestrator/src/mcp/types.ts @@ -0,0 +1,73 @@ +import type { ProfileIntegrationGrant } from "../db/schema.ts"; +import type { McpConfig } from "./config.ts"; +import { assertSchemaShape } from "./catalog.ts"; + +export interface McpToolGrant { + upstreamName: string; + automationWrite: boolean; + argumentConstraints: Record; + /** Required affirmative choice when constraints are empty. */ + unrestricted: boolean; +} +export interface McpConnectionState { + configRevision: number; + authGeneration: number; + credentialIdentity: string; + catalogRevision: number; + revoked: boolean; +} +export interface McpDiscoveredTool { + upstreamName: string; + description: string; + inputSchema: Record; + outputSchema?: Record; +} +export interface McpReviewedTool extends McpDiscoveredTool { + classification: "read" | "write"; + enabled: boolean; + fingerprint: string; +} +export interface McpBoundTool { + name: string; + tool: McpReviewedTool; + grant: McpToolGrant; +} +export interface McpBinding extends McpConnectionState { + connectionId: string; + config: McpConfig; + tools: McpBoundTool[]; + policyHash: string; + hash: string; +} +export type McpExecutionState = + | "waiting_approval" + | "authorized" + | "dispatching" + | "succeeded" + | "failed" + | "canceled" + | "outcome_unknown"; + +export function validateMcpGrant(grant: ProfileIntegrationGrant): void { + if ( + grant.operation !== "invoke" || + grant.credentialScope !== "org" || + grant.resourceConstraints.length !== 0 || + !grant.mcp + ) { + throw new Error("MCP grants require invoke, explicit org credentials, and a tool grant"); + } + const tool = grant.mcp; + if ( + !tool.upstreamName || + tool.upstreamName.length > 256 || + /[\u0000-\u001f]/.test(tool.upstreamName) || + typeof tool.automationWrite !== "boolean" || + typeof tool.unrestricted !== "boolean" + ) + throw new Error("invalid MCP tool grant"); + assertSchemaShape(tool.argumentConstraints); + const empty = Object.keys(tool.argumentConstraints).length === 0; + if (empty !== tool.unrestricted) + throw new Error("empty constraints require an explicit unrestricted choice"); +} diff --git a/orchestrator/src/rpc/integration.ts b/orchestrator/src/rpc/integration.ts index 79ed128aa..fb5e80322 100644 --- a/orchestrator/src/rpc/integration.ts +++ b/orchestrator/src/rpc/integration.ts @@ -11,6 +11,10 @@ * Injectable deps (getSession, connectors) for tests. */ +import { + mcpHandlers, mcpConfigFromProto, mcpConfigToProto, + requireMcp, requireMcpHook, mcpOperation, type McpServices, +} from "../mcp/contracts.ts"; import { ConnectError, Code } from "@connectrpc/connect"; import type { ConnectRouter } from "@connectrpc/connect"; @@ -125,6 +129,7 @@ export interface IntegrationOpAccess { } export interface IntegrationDeps { + mcp?: McpServices; getSession?: GetSession; connectors?: ConnectorStore; connectorLogos?: ConnectorLogoStore; @@ -305,6 +310,7 @@ function connectionToProto(row: IntegrationConnectionRow) { createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), ...config, + ...(row.provider === "mcp" ? { mcp: mcpConfigToProto(row.config), ...row.mcp } : {}), }; } @@ -400,6 +406,7 @@ export function registerIntegration(router: ConnectRouter, deps?: IntegrationDep const requireConnection = (id: string) => requireConnectionIn(providers, connections, id); router.service(IntegrationService, { + ...mcpHandlers({ services: deps?.mcp, connections, getSession, now, connectionToProto }), async listConnectors(_req, ctx) { await requireAdmin(ctx, getSession); const statusInputs = await fetchStatusInputs(orgSecret, mint, oauthCredential); @@ -729,6 +736,19 @@ export function registerIntegration(router: ConnectRouter, deps?: IntegrationDep async createConnection(req, ctx) { await requireAdmin(ctx, getSession); + if (req.provider === "mcp") { + const service = requireMcp(deps?.mcp); + const validate = requireMcpHook(service.validateDestination); + assertConnectionNames(req.alias, req.displayName); + const config = mcpConfigFromProto(req.mcp); + return mcpOperation(async () => { + await validate(config); + const row = await connections.create({ + alias: req.alias, provider: "mcp", displayName: req.displayName.trim(), config, + }); + return { connection: connectionToProto(row) }; + }); + } const provider = requireProvider(req.provider); provider.assertDeploymentReady?.({ issuer, deploymentId }); assertConnectionNames(req.alias, req.displayName); @@ -745,6 +765,19 @@ export function registerIntegration(router: ConnectRouter, deps?: IntegrationDep async updateConnection(req, ctx) { await requireAdmin(ctx, getSession); assertConnectionNames(req.alias, req.displayName); + const current = await connections.get(req.id); + if (current?.provider === "mcp") { + const service = requireMcp(deps?.mcp); + const validate = requireMcpHook(service.validateDestination); + const config = mcpConfigFromProto(req.mcp); + return mcpOperation(async () => { + await validate(config); + await service.repository.updateConfig(req.id, req.expectedRevision, { + alias: req.alias, displayName: req.displayName.trim(), config, + }, now()); + return { connection: connectionToProto((await connections.get(req.id))!) }; + }); + } const { provider } = await requireConnection(req.id); provider.assertDeploymentReady?.({ issuer, deploymentId }); const config = configFromProto(provider, req); @@ -799,6 +832,13 @@ export function registerIntegration(router: ConnectRouter, deps?: IntegrationDep await requireAdmin(ctx, getSession); const current = await connections.get(req.id); if (!current) throw new ConnectError("connection not found", Code.NotFound); + if (current.provider === "mcp") { + const service = requireMcp(deps?.mcp); + return mcpOperation(async () => { + await service.repository.setEnabled(req.id, req.expectedRevision, req.enabled, now()); + return { connection: connectionToProto((await connections.get(req.id))!) }; + }); + } if (req.enabled) { providers.get(current.provider)?.assertDeploymentReady?.({ issuer, deploymentId }); } diff --git a/orchestrator/src/rpc/profiles.ts b/orchestrator/src/rpc/profiles.ts index d9ff7a070..f7f337b20 100644 --- a/orchestrator/src/rpc/profiles.ts +++ b/orchestrator/src/rpc/profiles.ts @@ -132,6 +132,12 @@ function toProto(row: ProfileRow, isAdmin: boolean): Profile { operation: grant.operation, resourceConstraints: grant.resourceConstraints, credentialScope: grant.credentialScope ?? "", + mcp: grant.mcp ? { + upstreamName: grant.mcp.upstreamName, + automationWrite: grant.mcp.automationWrite, + argumentConstraintsJson: JSON.stringify(grant.mcp.argumentConstraints), + unrestricted: grant.mcp.unrestricted, + } : undefined, })), // ADR 0057: network + secrets describe access/config (the secret VALUES // live in the org store, never here), so they're member-visible like skills. @@ -486,6 +492,7 @@ export function registerProfiles(router: ConnectRouter, deps?: ProfileDeps): voi operation?: string; resourceConstraints?: string[]; credentialScope?: string; + mcp?: { upstreamName: string; automationWrite: boolean; argumentConstraintsJson: string; unrestricted: boolean }; }>, ): ProfileIntegrationGrant[] { return grants.map((grant) => { @@ -496,12 +503,21 @@ export function registerProfiles(router: ConnectRouter, deps?: ProfileDeps): voi Code.InvalidArgument, ); } + let mcp: ProfileIntegrationGrant["mcp"]; + if (grant.mcp) { + let constraints: unknown; + try { constraints = JSON.parse(grant.mcp.argumentConstraintsJson); } + catch { throw new ConnectError("MCP constraints must be valid JSON", Code.InvalidArgument); } + if (!constraints || typeof constraints !== "object" || Array.isArray(constraints)) throw new ConnectError("MCP constraints must be a JSON object", Code.InvalidArgument); + mcp = { upstreamName: grant.mcp.upstreamName, automationWrite: grant.mcp.automationWrite, argumentConstraints: constraints as Record, unrestricted: grant.mcp.unrestricted }; + } return { + ...(mcp ? { mcp } : {}), connectionId: (grant.connectionId ?? "").trim(), operation: (grant.operation ?? "").trim(), resourceConstraints: [...new Set(grant.resourceConstraints ?? [])].sort(), // Stored only when user-scoped; absent means org (the default). - ...(scope === "user" ? { credentialScope: "user" as const } : {}), + ...(scope === "user" ? { credentialScope: "user" as const } : scope === "org" ? { credentialScope: "org" as const } : {}), }; }); } diff --git a/orchestrator/src/rpc/task-create.ts b/orchestrator/src/rpc/task-create.ts index 24017175d..86d958bd4 100644 --- a/orchestrator/src/rpc/task-create.ts +++ b/orchestrator/src/rpc/task-create.ts @@ -490,6 +490,11 @@ export async function compileSessionCreateInput( ? overrideGrants : [...profile.integrationGrants, ...overrideGrants]; const resolvedEffectiveGrants = await resolveIntegrationGrants(effectiveGrants, connections); + // The runtime must install managed-host denies and session-local tool bindings + // before any MCP authority is exposed. That activation is not configured yet. + if (resolvedEffectiveGrants.some(({ connection }) => connection.provider === "mcp")) { + throw new ConnectError("external MCP session activation is not configured", Code.FailedPrecondition); + } const disabledConnection = resolvedEffectiveGrants.find(({ connection }) => !connection.enabled); if (disabledConnection) { throw new ConnectError( diff --git a/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts b/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts index 7d712c650..602562582 100644 --- a/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts +++ b/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts @@ -115,3 +115,48 @@ export const setConnectionEnabled = IntegrationService.method.setConnectionEnabl * @generated from rpc engram.app.v1.IntegrationService.GetGoogleCloudSetup */ export const getGoogleCloudSetup = IntegrationService.method.getGoogleCloudSetup; + +/** + * @generated from rpc engram.app.v1.IntegrationService.DiscoverMcp + */ +export const discoverMcp = IntegrationService.method.discoverMcp; + +/** + * @generated from rpc engram.app.v1.IntegrationService.PublishMcp + */ +export const publishMcp = IntegrationService.method.publishMcp; + +/** + * @generated from rpc engram.app.v1.IntegrationService.AuthorizeMcp + */ +export const authorizeMcp = IntegrationService.method.authorizeMcp; + +/** + * @generated from rpc engram.app.v1.IntegrationService.DisconnectMcp + */ +export const disconnectMcp = IntegrationService.method.disconnectMcp; + +/** + * @generated from rpc engram.app.v1.IntegrationService.ListMcpApprovals + */ +export const listMcpApprovals = IntegrationService.method.listMcpApprovals; + +/** + * @generated from rpc engram.app.v1.IntegrationService.DecideMcpApproval + */ +export const decideMcpApproval = IntegrationService.method.decideMcpApproval; + +/** + * @generated from rpc engram.app.v1.IntegrationService.CancelMcpCall + */ +export const cancelMcpCall = IntegrationService.method.cancelMcpCall; + +/** + * @generated from rpc engram.app.v1.IntegrationService.ResolveMcpOutcome + */ +export const resolveMcpOutcome = IntegrationService.method.resolveMcpOutcome; + +/** + * @generated from rpc engram.app.v1.IntegrationService.RebindMcp + */ +export const rebindMcp = IntegrationService.method.rebindMcp; diff --git a/web/src/gen/engram/app/v1/integration_pb.ts b/web/src/gen/engram/app/v1/integration_pb.ts index aa87250b6..acd7513f6 100644 --- a/web/src/gen/engram/app/v1/integration_pb.ts +++ b/web/src/gen/engram/app/v1/integration_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/integration.proto. */ export const file_engram_app_v1_integration: GenFile = /*@__PURE__*/ - fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAki/AEKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIpIBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWciVAoYQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiKMAQoXVXBkYXRlQ29ubmVjdGlvblJlcXVlc3QSCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnIlQKGFVwZGF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24iJQoXRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QSCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSI6ChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCCJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IynQwKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZWIGcHJvdG8z"); + fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i2AEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAYgASgNEgoKAmlkGAEgASgJEg0KBWFsaWFzGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRJACgxnb29nbGVfY2xvdWQYBCABKAsyKi5lbmdyYW0uYXBwLnYxLkdvb2dsZUNsb3VkQ29ubmVjdGlvbkNvbmZpZxIvCgNtY3AYBSABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENvbm5lY3Rpb25Db25maWciVAoYVXBkYXRlQ29ubmVjdGlvblJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIlChdEZWxldGVDb25uZWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCSIrChhEZWxldGVDb25uZWN0aW9uUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCIjChVUZXN0Q29ubmVjdGlvblJlcXVlc3QSCgoCaWQYASABKAkiNQoWVGVzdENvbm5lY3Rpb25SZXNwb25zZRIKCgJvaxgBIAEoCBIPCgdtZXNzYWdlGAIgASgJIlUKG1NldENvbm5lY3Rpb25FbmFibGVkUmVxdWVzdBIKCgJpZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNIlgKHFNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIigKGkdldEdvb2dsZUNsb3VkU2V0dXBSZXF1ZXN0EgoKAmlkGAEgASgJIqIBChtHZXRHb29nbGVDbG91ZFNldHVwUmVzcG9uc2USDgoGaXNzdWVyGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEhkKEXN1YmplY3RfYXR0cmlidXRlGAMgASgJEhwKFGNvbm5lY3Rpb25fYXR0cmlidXRlGAQgASgJEhUKDWdjbG91ZF9zY3JpcHQYBSABKAkSEQoJdGVycmFmb3JtGAYgASgJIuYBCglDb25uZWN0b3ISEAoIcHJvdmlkZXIYASABKAkSEwoLY29uZmlnX2pzb24YAiABKAkSDwoHYnVpbHRpbhgDIAEoCBISCgpjcmVhdGVkX2F0GAQgASgJEhIKCnVwZGF0ZWRfYXQYBSABKAkSDgoGc3RhdHVzGAYgASgJEjgKCHNldHRpbmdzGAcgAygLMiYuZW5ncmFtLmFwcC52MS5Db25uZWN0b3IuU2V0dGluZ3NFbnRyeRovCg1TZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiFwoVTGlzdENvbm5lY3RvcnNSZXF1ZXN0IkYKFkxpc3RDb25uZWN0b3JzUmVzcG9uc2USLAoKY29ubmVjdG9ycxgBIAMoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIi0KFlVwc2VydENvbm5lY3RvclJlcXVlc3QSEwoLY29uZmlnX2pzb24YASABKAkiRgoXVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiKgoWRGVsZXRlQ29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCSIqChdEZWxldGVDb25uZWN0b3JSZXNwb25zZRIPCgdkZWxldGVkGAEgASgIIjoKDUNvbm5lY3Rvckljb24SDAoEbW9ubxgBIAEoCRINCgVjb2xvchgCIAEoCRIMCgRsb2dvGAMgASgJIn8KEENvbm5lY3RvckRpc3BsYXkSDAoEbmFtZRgBIAEoCRIQCghjYXRlZ29yeRgCIAEoCRINCgVibHVyYhgDIAEoCRIqCgRpY29uGAQgASgLMhwuZW5ncmFtLmFwcC52MS5Db25uZWN0b3JJY29uEhAKCGZlYXR1cmVkGAUgASgIInYKEUNhdGFsb2dDYXBhYmlsaXR5Eg4KBmFjdGlvbhgBIAEoCRIOCgZhY2Nlc3MYAiABKAkSDQoFYXNzZXQYAyABKAkSDQoFbGFiZWwYBCABKAkSDAoEaG9zdBgFIAEoCRIVCg1lbmRwb2ludF9ydWxlGAYgASgJIrQCChRQcm92aWRlckNhdGFsb2dFbnRyeRIQCghwcm92aWRlchgBIAEoCRIwCgdkaXNwbGF5GAIgASgLMh8uZW5ncmFtLmFwcC52MS5Db25uZWN0b3JEaXNwbGF5EhkKEWNyZWRlbnRpYWxfc291cmNlGAMgASgJEg0KBWhvc3RzGAQgAygJEjYKDGNhcGFiaWxpdGllcxgFIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ2F0YWxvZ0NhcGFiaWxpdHkSHQoVZGVmYXVsdF9jb25uZWN0aW9uX2lkGAYgASgJEhgKEGNvbm5lY3Rpb25fbW9kZWwYByABKAkSPQoPdXNlcl9jcmVkZW50aWFsGAggASgLMiQuZW5ncmFtLmFwcC52MS5Vc2VyQ3JlZGVudGlhbFN1cHBvcnQiSQoVVXNlckNyZWRlbnRpYWxTdXBwb3J0Eg0KBW9hdXRoGAEgASgIEg0KBXRva2VuGAIgASgIEhIKCnRva2VuX2hpbnQYAyABKAkiHgocR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVxdWVzdCJXCh1HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXNwb25zZRI2Cglwcm92aWRlcnMYASADKAsyIy5lbmdyYW0uYXBwLnYxLlByb3ZpZGVyQ2F0YWxvZ0VudHJ5Iq4BChhTZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkSDAoEa2luZBgCIAEoCRJDCgZ2YWx1ZXMYAyADKAsyMy5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVxdWVzdC5WYWx1ZXNFbnRyeRotCgtWYWx1ZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjEKGVNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USFAoMc2VjcmV0X25hbWVzGAEgAygJIlAKGlVwbG9hZENvbm5lY3RvckxvZ29SZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGRhdGEYAiABKAwSEgoKbWVkaWFfdHlwZRgDIAEoCSIvChtVcGxvYWRDb25uZWN0b3JMb2dvUmVzcG9uc2USEAoIbG9nb191cmwYASABKAkirgIKFFRlc3RDb25uZWN0b3JSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEkoKDGRyYWZ0X3ZhbHVlcxgCIAMoCzI0LmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QuRHJhZnRWYWx1ZXNFbnRyeRJOCg5kcmFmdF9zZXR0aW5ncxgDIAMoCzI2LmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QuRHJhZnRTZXR0aW5nc0VudHJ5GjIKEERyYWZ0VmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ARo0ChJEcmFmdFNldHRpbmdzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI0ChVUZXN0Q29ubmVjdG9yUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSKsAQobU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEkoKCHNldHRpbmdzGAIgAygLMjguZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QuU2V0dGluZ3NFbnRyeRovCg1TZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiSwocU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXNwb25zZRIrCgljb25uZWN0b3IYASABKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciKrAQoTTWNwQ29ubmVjdGlvbkNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIMCgR6b25lGAIgASgJEhEKCWF1dGhfa2luZBgDIAEoCRIaChJhcGlfa2V5X3NlY3JldF9yZWYYBCABKAkSJAocb2F1dGhfY2xpZW50X3JlZ2lzdHJhdGlvbl9pZBgFIAEoCRIfChdzaGFyZV93aXRoX29yZ19zZXNzaW9ucxgGIAEoCCKqAQoHTWNwVG9vbBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhkKEWlucHV0X3NjaGVtYV9qc29uGAMgASgJEhoKEm91dHB1dF9zY2hlbWFfanNvbhgEIAEoCRIWCg5jbGFzc2lmaWNhdGlvbhgFIAEoCRIPCgdlbmFibGVkGAYgASgIEhMKC2ZpbmdlcnByaW50GAcgASgJIr0BCgpNY3BDYXRhbG9nEgoKAmlkGAEgASgJEhUKDWNvbm5lY3Rpb25faWQYAiABKAkSDAoEa2luZBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoDRIXCg9jb25maWdfcmV2aXNpb24YBSABKA0SFwoPYXV0aF9nZW5lcmF0aW9uGAYgASgNEiUKBXRvb2xzGAcgAygLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEhMKC2ZpbmdlcnByaW50GAggASgJIkYKEkRpc2NvdmVyTWNwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNIkEKE0Rpc2NvdmVyTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyJ+ChFQdWJsaXNoTWNwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhAKCGRyYWZ0X2lkGAIgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEiUKBXRvb2xzGAQgAygLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sIkAKElB1Ymxpc2hNY3BSZXNwb25zZRIqCgdjYXRhbG9nGAEgASgLMhkuZW5ncmFtLmFwcC52MS5NY3BDYXRhbG9nImoKE0F1dGhvcml6ZU1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAIgASgNEh8KF3NoYXJlX3dpdGhfb3JnX3Nlc3Npb25zGAMgASgIIjEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJIkoKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIs4BCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkiLQoXTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJJChhMaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USLQoJYXBwcm92YWxzGAEgAygLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbCJBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgiKgoZRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRINCgVzdGF0ZRgBIAEoCSIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAky3BIKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USVwoMQXV0aG9yaXplTWNwEiIuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXNwb25zZRJaCg1EaXNjb25uZWN0TWNwEiMuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGlzY29ubmVjdE1jcFJlc3BvbnNlEmMKEExpc3RNY3BBcHByb3ZhbHMSJi5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5MaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USZgoRRGVjaWRlTWNwQXBwcm92YWwSJy5lbmdyYW0uYXBwLnYxLkRlY2lkZU1jcEFwcHJvdmFsUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRJaCg1DYW5jZWxNY3BDYWxsEiMuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEmYKEVJlc29sdmVNY3BPdXRjb21lEicuZW5ncmFtLmFwcC52MS5SZXNvbHZlTWNwT3V0Y29tZVJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVzcG9uc2USTgoJUmViaW5kTWNwEh8uZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXNwb25zZWIGcHJvdG8z"); /** * @generated from message engram.app.v1.GoogleCloudConnectionConfig @@ -106,6 +106,36 @@ export type IntegrationConnection = Message<"engram.app.v1.IntegrationConnection * @generated from field: bool is_default = 10; */ isDefault: boolean; + + /** + * @generated from field: engram.app.v1.McpConnectionConfig mcp = 11; + */ + mcp?: McpConnectionConfig; + + /** + * @generated from field: uint32 config_revision = 12; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 13; + */ + authGeneration: number; + + /** + * @generated from field: string credential_identity = 14; + */ + credentialIdentity: string; + + /** + * @generated from field: uint32 catalog_revision = 15; + */ + catalogRevision: number; + + /** + * @generated from field: bool revoked = 16; + */ + revoked: boolean; }; /** @@ -168,6 +198,11 @@ export type CreateConnectionRequest = Message<"engram.app.v1.CreateConnectionReq * @generated from field: engram.app.v1.GoogleCloudConnectionConfig google_cloud = 4; */ googleCloud?: GoogleCloudConnectionConfig; + + /** + * @generated from field: engram.app.v1.McpConnectionConfig mcp = 5; + */ + mcp?: McpConnectionConfig; }; /** @@ -198,6 +233,11 @@ export const CreateConnectionResponseSchema: GenMessage & { + /** + * @generated from field: uint32 expected_revision = 6; + */ + expectedRevision: number; + /** * @generated from field: string id = 1; */ @@ -217,6 +257,11 @@ export type UpdateConnectionRequest = Message<"engram.app.v1.UpdateConnectionReq * @generated from field: engram.app.v1.GoogleCloudConnectionConfig google_cloud = 4; */ googleCloud?: GoogleCloudConnectionConfig; + + /** + * @generated from field: engram.app.v1.McpConnectionConfig mcp = 5; + */ + mcp?: McpConnectionConfig; }; /** @@ -329,6 +374,11 @@ export type SetConnectionEnabledRequest = Message<"engram.app.v1.SetConnectionEn * @generated from field: bool enabled = 2; */ enabled: boolean; + + /** + * @generated from field: uint32 expected_revision = 3; + */ + expectedRevision: number; }; /** @@ -1034,6 +1084,564 @@ export type SetConnectorSettingsResponse = Message<"engram.app.v1.SetConnectorSe export const SetConnectorSettingsResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_integration, 37); +/** + * Organization credential references only. Secret values never enter this contract. + * + * @generated from message engram.app.v1.McpConnectionConfig + */ +export type McpConnectionConfig = Message<"engram.app.v1.McpConnectionConfig"> & { + /** + * @generated from field: string endpoint = 1; + */ + endpoint: string; + + /** + * @generated from field: string zone = 2; + */ + zone: string; + + /** + * @generated from field: string auth_kind = 3; + */ + authKind: string; + + /** + * @generated from field: string api_key_secret_ref = 4; + */ + apiKeySecretRef: string; + + /** + * @generated from field: string oauth_client_registration_id = 5; + */ + oauthClientRegistrationId: string; + + /** + * @generated from field: bool share_with_org_sessions = 6; + */ + shareWithOrgSessions: boolean; +}; + +/** + * Describes the message engram.app.v1.McpConnectionConfig. + * Use `create(McpConnectionConfigSchema)` to create a new message. + */ +export const McpConnectionConfigSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 38); + +/** + * @generated from message engram.app.v1.McpTool + */ +export type McpTool = Message<"engram.app.v1.McpTool"> & { + /** + * @generated from field: string upstream_name = 1; + */ + upstreamName: string; + + /** + * @generated from field: string description = 2; + */ + description: string; + + /** + * @generated from field: string input_schema_json = 3; + */ + inputSchemaJson: string; + + /** + * @generated from field: string output_schema_json = 4; + */ + outputSchemaJson: string; + + /** + * Empty in discovery drafts. Publication requires explicit read or write. + * + * @generated from field: string classification = 5; + */ + classification: string; + + /** + * @generated from field: bool enabled = 6; + */ + enabled: boolean; + + /** + * @generated from field: string fingerprint = 7; + */ + fingerprint: string; +}; + +/** + * Describes the message engram.app.v1.McpTool. + * Use `create(McpToolSchema)` to create a new message. + */ +export const McpToolSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 39); + +/** + * @generated from message engram.app.v1.McpCatalog + */ +export type McpCatalog = Message<"engram.app.v1.McpCatalog"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string connection_id = 2; + */ + connectionId: string; + + /** + * @generated from field: string kind = 3; + */ + kind: string; + + /** + * @generated from field: uint32 revision = 4; + */ + revision: number; + + /** + * @generated from field: uint32 config_revision = 5; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 6; + */ + authGeneration: number; + + /** + * @generated from field: repeated engram.app.v1.McpTool tools = 7; + */ + tools: McpTool[]; + + /** + * @generated from field: string fingerprint = 8; + */ + fingerprint: string; +}; + +/** + * Describes the message engram.app.v1.McpCatalog. + * Use `create(McpCatalogSchema)` to create a new message. + */ +export const McpCatalogSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 40); + +/** + * @generated from message engram.app.v1.DiscoverMcpRequest + */ +export type DiscoverMcpRequest = Message<"engram.app.v1.DiscoverMcpRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; +}; + +/** + * Describes the message engram.app.v1.DiscoverMcpRequest. + * Use `create(DiscoverMcpRequestSchema)` to create a new message. + */ +export const DiscoverMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 41); + +/** + * @generated from message engram.app.v1.DiscoverMcpResponse + */ +export type DiscoverMcpResponse = Message<"engram.app.v1.DiscoverMcpResponse"> & { + /** + * @generated from field: engram.app.v1.McpCatalog catalog = 1; + */ + catalog?: McpCatalog; +}; + +/** + * Describes the message engram.app.v1.DiscoverMcpResponse. + * Use `create(DiscoverMcpResponseSchema)` to create a new message. + */ +export const DiscoverMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 42); + +/** + * @generated from message engram.app.v1.PublishMcpRequest + */ +export type PublishMcpRequest = Message<"engram.app.v1.PublishMcpRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: string draft_id = 2; + */ + draftId: string; + + /** + * @generated from field: uint32 expected_revision = 3; + */ + expectedRevision: number; + + /** + * @generated from field: repeated engram.app.v1.McpTool tools = 4; + */ + tools: McpTool[]; +}; + +/** + * Describes the message engram.app.v1.PublishMcpRequest. + * Use `create(PublishMcpRequestSchema)` to create a new message. + */ +export const PublishMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 43); + +/** + * @generated from message engram.app.v1.PublishMcpResponse + */ +export type PublishMcpResponse = Message<"engram.app.v1.PublishMcpResponse"> & { + /** + * @generated from field: engram.app.v1.McpCatalog catalog = 1; + */ + catalog?: McpCatalog; +}; + +/** + * Describes the message engram.app.v1.PublishMcpResponse. + * Use `create(PublishMcpResponseSchema)` to create a new message. + */ +export const PublishMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 44); + +/** + * @generated from message engram.app.v1.AuthorizeMcpRequest + */ +export type AuthorizeMcpRequest = Message<"engram.app.v1.AuthorizeMcpRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_generation = 2; + */ + expectedGeneration: number; + + /** + * @generated from field: bool share_with_org_sessions = 3; + */ + shareWithOrgSessions: boolean; +}; + +/** + * Describes the message engram.app.v1.AuthorizeMcpRequest. + * Use `create(AuthorizeMcpRequestSchema)` to create a new message. + */ +export const AuthorizeMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 45); + +/** + * @generated from message engram.app.v1.AuthorizeMcpResponse + */ +export type AuthorizeMcpResponse = Message<"engram.app.v1.AuthorizeMcpResponse"> & { + /** + * @generated from field: string authorization_url = 1; + */ + authorizationUrl: string; +}; + +/** + * Describes the message engram.app.v1.AuthorizeMcpResponse. + * Use `create(AuthorizeMcpResponseSchema)` to create a new message. + */ +export const AuthorizeMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 46); + +/** + * @generated from message engram.app.v1.DisconnectMcpRequest + */ +export type DisconnectMcpRequest = Message<"engram.app.v1.DisconnectMcpRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_generation = 2; + */ + expectedGeneration: number; +}; + +/** + * Describes the message engram.app.v1.DisconnectMcpRequest. + * Use `create(DisconnectMcpRequestSchema)` to create a new message. + */ +export const DisconnectMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 47); + +/** + * @generated from message engram.app.v1.DisconnectMcpResponse + */ +export type DisconnectMcpResponse = Message<"engram.app.v1.DisconnectMcpResponse"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; +}; + +/** + * Describes the message engram.app.v1.DisconnectMcpResponse. + * Use `create(DisconnectMcpResponseSchema)` to create a new message. + */ +export const DisconnectMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 48); + +/** + * @generated from message engram.app.v1.McpApproval + */ +export type McpApproval = Message<"engram.app.v1.McpApproval"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 4; + */ + toolCallId: string; + + /** + * @generated from field: string connection_id = 5; + */ + connectionId: string; + + /** + * @generated from field: string upstream_name = 6; + */ + upstreamName: string; + + /** + * @generated from field: string request_digest = 7; + */ + requestDigest: string; + + /** + * @generated from field: string argument_hash = 8; + */ + argumentHash: string; + + /** + * @generated from field: string expires_at = 9; + */ + expiresAt: string; +}; + +/** + * Describes the message engram.app.v1.McpApproval. + * Use `create(McpApprovalSchema)` to create a new message. + */ +export const McpApprovalSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 49); + +/** + * @generated from message engram.app.v1.ListMcpApprovalsRequest + */ +export type ListMcpApprovalsRequest = Message<"engram.app.v1.ListMcpApprovalsRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; +}; + +/** + * Describes the message engram.app.v1.ListMcpApprovalsRequest. + * Use `create(ListMcpApprovalsRequestSchema)` to create a new message. + */ +export const ListMcpApprovalsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 50); + +/** + * @generated from message engram.app.v1.ListMcpApprovalsResponse + */ +export type ListMcpApprovalsResponse = Message<"engram.app.v1.ListMcpApprovalsResponse"> & { + /** + * @generated from field: repeated engram.app.v1.McpApproval approvals = 1; + */ + approvals: McpApproval[]; +}; + +/** + * Describes the message engram.app.v1.ListMcpApprovalsResponse. + * Use `create(ListMcpApprovalsResponseSchema)` to create a new message. + */ +export const ListMcpApprovalsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 51); + +/** + * @generated from message engram.app.v1.DecideMcpApprovalRequest + */ +export type DecideMcpApprovalRequest = Message<"engram.app.v1.DecideMcpApprovalRequest"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: bool approve = 2; + */ + approve: boolean; +}; + +/** + * Describes the message engram.app.v1.DecideMcpApprovalRequest. + * Use `create(DecideMcpApprovalRequestSchema)` to create a new message. + */ +export const DecideMcpApprovalRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 52); + +/** + * @generated from message engram.app.v1.DecideMcpApprovalResponse + */ +export type DecideMcpApprovalResponse = Message<"engram.app.v1.DecideMcpApprovalResponse"> & { + /** + * @generated from field: string state = 1; + */ + state: string; +}; + +/** + * Describes the message engram.app.v1.DecideMcpApprovalResponse. + * Use `create(DecideMcpApprovalResponseSchema)` to create a new message. + */ +export const DecideMcpApprovalResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 53); + +/** + * @generated from message engram.app.v1.CancelMcpCallRequest + */ +export type CancelMcpCallRequest = Message<"engram.app.v1.CancelMcpCallRequest"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; +}; + +/** + * Describes the message engram.app.v1.CancelMcpCallRequest. + * Use `create(CancelMcpCallRequestSchema)` to create a new message. + */ +export const CancelMcpCallRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 54); + +/** + * @generated from message engram.app.v1.CancelMcpCallResponse + */ +export type CancelMcpCallResponse = Message<"engram.app.v1.CancelMcpCallResponse"> & { + /** + * @generated from field: string state = 1; + */ + state: string; +}; + +/** + * Describes the message engram.app.v1.CancelMcpCallResponse. + * Use `create(CancelMcpCallResponseSchema)` to create a new message. + */ +export const CancelMcpCallResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 55); + +/** + * @generated from message engram.app.v1.ResolveMcpOutcomeRequest + */ +export type ResolveMcpOutcomeRequest = Message<"engram.app.v1.ResolveMcpOutcomeRequest"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string outcome = 2; + */ + outcome: string; +}; + +/** + * Describes the message engram.app.v1.ResolveMcpOutcomeRequest. + * Use `create(ResolveMcpOutcomeRequestSchema)` to create a new message. + */ +export const ResolveMcpOutcomeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 56); + +/** + * @generated from message engram.app.v1.ResolveMcpOutcomeResponse + */ +export type ResolveMcpOutcomeResponse = Message<"engram.app.v1.ResolveMcpOutcomeResponse"> & { + /** + * @generated from field: string outcome = 1; + */ + outcome: string; +}; + +/** + * Describes the message engram.app.v1.ResolveMcpOutcomeResponse. + * Use `create(ResolveMcpOutcomeResponseSchema)` to create a new message. + */ +export const ResolveMcpOutcomeResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 57); + +/** + * @generated from message engram.app.v1.RebindMcpRequest + */ +export type RebindMcpRequest = Message<"engram.app.v1.RebindMcpRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string expected_binding_hash = 2; + */ + expectedBindingHash: string; +}; + +/** + * Describes the message engram.app.v1.RebindMcpRequest. + * Use `create(RebindMcpRequestSchema)` to create a new message. + */ +export const RebindMcpRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 58); + +/** + * @generated from message engram.app.v1.RebindMcpResponse + */ +export type RebindMcpResponse = Message<"engram.app.v1.RebindMcpResponse"> & { + /** + * @generated from field: string binding_hash = 1; + */ + bindingHash: string; +}; + +/** + * Describes the message engram.app.v1.RebindMcpResponse. + * Use `create(RebindMcpResponseSchema)` to create a new message. + */ +export const RebindMcpResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 59); + /** * ADR 0057 C3: orchestrator-native connector catalog CRUD (Plane B). Connectors * live in the orchestrator DB (the `connector` table, C1); the built-ins @@ -1202,6 +1810,78 @@ export const IntegrationService: GenService<{ input: typeof GetGoogleCloudSetupRequestSchema; output: typeof GetGoogleCloudSetupResponseSchema; }, + /** + * @generated from rpc engram.app.v1.IntegrationService.DiscoverMcp + */ + discoverMcp: { + methodKind: "unary"; + input: typeof DiscoverMcpRequestSchema; + output: typeof DiscoverMcpResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.PublishMcp + */ + publishMcp: { + methodKind: "unary"; + input: typeof PublishMcpRequestSchema; + output: typeof PublishMcpResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.AuthorizeMcp + */ + authorizeMcp: { + methodKind: "unary"; + input: typeof AuthorizeMcpRequestSchema; + output: typeof AuthorizeMcpResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.DisconnectMcp + */ + disconnectMcp: { + methodKind: "unary"; + input: typeof DisconnectMcpRequestSchema; + output: typeof DisconnectMcpResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.ListMcpApprovals + */ + listMcpApprovals: { + methodKind: "unary"; + input: typeof ListMcpApprovalsRequestSchema; + output: typeof ListMcpApprovalsResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.DecideMcpApproval + */ + decideMcpApproval: { + methodKind: "unary"; + input: typeof DecideMcpApprovalRequestSchema; + output: typeof DecideMcpApprovalResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.CancelMcpCall + */ + cancelMcpCall: { + methodKind: "unary"; + input: typeof CancelMcpCallRequestSchema; + output: typeof CancelMcpCallResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.ResolveMcpOutcome + */ + resolveMcpOutcome: { + methodKind: "unary"; + input: typeof ResolveMcpOutcomeRequestSchema; + output: typeof ResolveMcpOutcomeResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.RebindMcp + */ + rebindMcp: { + methodKind: "unary"; + input: typeof RebindMcpRequestSchema; + output: typeof RebindMcpResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_engram_app_v1_integration, 0); diff --git a/web/src/gen/engram/app/v1/profile_pb.ts b/web/src/gen/engram/app/v1/profile_pb.ts index 1dfa02b2f..d939bc0ca 100644 --- a/web/src/gen/engram/app/v1/profile_pb.ts +++ b/web/src/gen/engram/app/v1/profile_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/profile.proto. */ export const file_engram_app_v1_profile: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8iewoXUHJvZmlsZUludGVncmF0aW9uR3JhbnQSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSHAoUcmVzb3VyY2VfY29uc3RyYWludHMYAyADKAkSGAoQY3JlZGVudGlhbF9zY29wZRgEIAEoCSJTCg5Qcm9maWxlTmV0d29yaxIPCgdkZWZhdWx0GAEgASgJEhMKC2FsbG93X2hvc3RzGAIgAygJEhsKE2FsbG93X2hvc3RfcGF0dGVybnMYAyADKAkibQoNUHJvZmlsZVNlY3JldBILCgNyZWYYASABKAkSDwoHZW52X3ZhchgCIAEoCRIMCgRtb2RlGAMgASgJEhMKC2FsbG93X2hvc3RzGAQgAygJEhsKE2FsbG93X2hvc3RfcGF0dGVybnMYBSADKAkibgoPUHJvZmlsZVNuYXBzaG90EgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghhcmNoaXZlZBgEIAEoCBIRCglpbWFnZV91cmkYBSABKAkSDgoGc2tpbGxzGAYgAygJIksKE0xpc3RQcm9maWxlc1JlcXVlc3QSGAoQaW5jbHVkZV9hcmNoaXZlZBgBIAEoCBIaChJmb3Jfc2Vzc2lvbl9waWNrZXIYAiABKAgiQAoUTGlzdFByb2ZpbGVzUmVzcG9uc2USKAoIcHJvZmlsZXMYASADKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiHwoRR2V0UHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiPQoSR2V0UHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUisAUKFENyZWF0ZVByb2ZpbGVSZXF1ZXN0EgwKBG5hbWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSDAoEaWNvbhgDIAEoCRIQCghpbWFnZV9pZBgEIAEoCRIbChNpbmNsdWRlX3VzZXJfdG9rZW5zGAUgASgIEkIKCGVudl92YXJzGAYgAygLMjAuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdC5FbnZWYXJzRW50cnkSDgoGc2tpbGxzGAcgAygJEi4KB25ldHdvcmsYCSABKAsyHS5lbmdyYW0uYXBwLnYxLlByb2ZpbGVOZXR3b3JrEi0KB3NlY3JldHMYCiADKAsyHC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVTZWNyZXQSFAoHaGFybmVzcxgMIAEoCUgAiAEBEhIKBW1vZGVsGA0gASgJSAGIAQESEwoGZWZmb3J0GA4gASgJSAKIAQESQgoSaW50ZWdyYXRpb25fZ3JhbnRzGBEgAygLMiYuZW5ncmFtLmFwcC52MS5Qcm9maWxlSW50ZWdyYXRpb25HcmFudBIpCgVyZXBvcxgSIAMoCzIaLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVJlcG8SGQoMbW9kZWxfcm91dGVyGBMgASgJSAOIAQESJwoEYXBwcxgUIAMoCzIZLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbkFwcBIZChFwcm9ncmFtbWF0aWNfb25seRgVIAEoCBouCgxFbnZWYXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfaGFybmVzc0IICgZfbW9kZWxCCQoHX2VmZm9ydEIPCg1fbW9kZWxfcm91dGVySgQICBAJSgQICxAMSgQIDxAQSgQIEBARIkAKFUNyZWF0ZVByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlItcFChRVcGRhdGVQcm9maWxlUmVxdWVzdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEgwKBGljb24YBCABKAkSEAoIaW1hZ2VfaWQYBSABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgGIAEoCBJCCghlbnZfdmFycxgHIAMoCzIwLmVuZ3JhbS5hcHAudjEuVXBkYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgIIAMoCRIuCgduZXR3b3JrGAogASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAsgAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDSABKAlIAIgBARISCgVtb2RlbBgOIAEoCUgBiAEBEhMKBmVmZm9ydBgPIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgSIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEyADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgUIAEoCUgDiAEBEicKBGFwcHMYFSADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASHgoRcHJvZ3JhbW1hdGljX29ubHkYFiABKAhIBIgBARouCgxFbnZWYXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfaGFybmVzc0IICgZfbW9kZWxCCQoHX2VmZm9ydEIPCg1fbW9kZWxfcm91dGVyQhQKEl9wcm9ncmFtbWF0aWNfb25seUoECAkQCkoECAwQDUoECBAQEUoECBEQEiJAChVVcGRhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSIiChREZWxldGVQcm9maWxlUmVxdWVzdBIKCgJpZBgBIAEoCSIXChVEZWxldGVQcm9maWxlUmVzcG9uc2UywQQKDlByb2ZpbGVTZXJ2aWNlElcKDExpc3RQcm9maWxlcxIiLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBojLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USUQoKR2V0UHJvZmlsZRIgLmVuZ3JhbS5hcHAudjEuR2V0UHJvZmlsZVJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXNwb25zZRJaCg1DcmVhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlc3BvbnNlEloKDVVwZGF0ZVByb2ZpbGUSIy5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVzcG9uc2USbwoURGlzY292ZXJQcm9maWxlUmVwb3MSKi5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRJaCg1EZWxldGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5EZWxldGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlc3BvbnNlYgZwcm90bzM"); + fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8ipQEKF1Byb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EhUKDWNvbm5lY3Rpb25faWQYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEhwKFHJlc291cmNlX2NvbnN0cmFpbnRzGAMgAygJEhgKEGNyZWRlbnRpYWxfc2NvcGUYBCABKAkSKAoDbWNwGAUgASgLMhsuZW5ncmFtLmFwcC52MS5NY3BUb29sR3JhbnQiUwoOUHJvZmlsZU5ldHdvcmsSDwoHZGVmYXVsdBgBIAEoCRITCgthbGxvd19ob3N0cxgCIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAMgAygJIm0KDVByb2ZpbGVTZWNyZXQSCwoDcmVmGAEgASgJEg8KB2Vudl92YXIYAiABKAkSDAoEbW9kZRgDIAEoCRITCgthbGxvd19ob3N0cxgEIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAUgAygJIm4KD1Byb2ZpbGVTbmFwc2hvdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGljb24YAyABKAkSEAoIYXJjaGl2ZWQYBCABKAgSEQoJaW1hZ2VfdXJpGAUgASgJEg4KBnNraWxscxgGIAMoCSJLChNMaXN0UHJvZmlsZXNSZXF1ZXN0EhgKEGluY2x1ZGVfYXJjaGl2ZWQYASABKAgSGgoSZm9yX3Nlc3Npb25fcGlja2VyGAIgASgIIkAKFExpc3RQcm9maWxlc1Jlc3BvbnNlEigKCHByb2ZpbGVzGAEgAygLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIh8KEUdldFByb2ZpbGVSZXF1ZXN0EgoKAmlkGAEgASgJIj0KEkdldFByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIrAFChRDcmVhdGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEgwKBGljb24YAyABKAkSEAoIaW1hZ2VfaWQYBCABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgFIAEoCBJCCghlbnZfdmFycxgGIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgHIAMoCRIuCgduZXR3b3JrGAkgASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAogAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDCABKAlIAIgBARISCgVtb2RlbBgNIAEoCUgBiAEBEhMKBmVmZm9ydBgOIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgRIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEiADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgTIAEoCUgDiAEBEicKBGFwcHMYFCADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASGQoRcHJvZ3JhbW1hdGljX29ubHkYFSABKAgaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSLXBQoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlKBAgJEApKBAgMEA1KBAgQEBFKBAgREBIiQAoVVXBkYXRlUHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiIgoURGVsZXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlUHJvZmlsZVJlc3BvbnNlIngKDE1jcFRvb2xHcmFudBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhgKEGF1dG9tYXRpb25fd3JpdGUYAiABKAgSIQoZYXJndW1lbnRfY29uc3RyYWludHNfanNvbhgDIAEoCRIUCgx1bnJlc3RyaWN0ZWQYBCABKAgywQQKDlByb2ZpbGVTZXJ2aWNlElcKDExpc3RQcm9maWxlcxIiLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBojLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USUQoKR2V0UHJvZmlsZRIgLmVuZ3JhbS5hcHAudjEuR2V0UHJvZmlsZVJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXNwb25zZRJaCg1DcmVhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlc3BvbnNlEloKDVVwZGF0ZVByb2ZpbGUSIy5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVzcG9uc2USbwoURGlzY292ZXJQcm9maWxlUmVwb3MSKi5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRJaCg1EZWxldGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5EZWxldGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlc3BvbnNlYgZwcm90bzM"); /** * An admin-curated bundle of session-launch inputs (ADR §1). @@ -394,6 +394,11 @@ export type ProfileIntegrationGrant = Message<"engram.app.v1.ProfileIntegrationG * @generated from field: string credential_scope = 4; */ credentialScope: string; + + /** + * @generated from field: engram.app.v1.McpToolGrant mcp = 5; + */ + mcp?: McpToolGrant; }; /** @@ -903,6 +908,38 @@ export type DeleteProfileResponse = Message<"engram.app.v1.DeleteProfileResponse export const DeleteProfileResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_profile, 21); +/** + * @generated from message engram.app.v1.McpToolGrant + */ +export type McpToolGrant = Message<"engram.app.v1.McpToolGrant"> & { + /** + * @generated from field: string upstream_name = 1; + */ + upstreamName: string; + + /** + * @generated from field: bool automation_write = 2; + */ + automationWrite: boolean; + + /** + * @generated from field: string argument_constraints_json = 3; + */ + argumentConstraintsJson: string; + + /** + * @generated from field: bool unrestricted = 4; + */ + unrestricted: boolean; +}; + +/** + * Describes the message engram.app.v1.McpToolGrant. + * Use `create(McpToolGrantSchema)` to create a new message. + */ +export const McpToolGrantSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_profile, 22); + /** * Session profiles (ADR 0053) — admin-curated session starting points. * ORCHESTRATOR-NATIVE: the control plane neither implements nor knows about From 8a36189c09906b2fad7a5ee20f71e89f431f28f6 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 21:23:18 -0500 Subject: [PATCH 004/279] Expire MCP calls interrupted before admission --- orchestrator/src/__tests__/mcp-rpc.test.ts | 1 + orchestrator/src/__tests__/mcp-store.test.ts | 29 ++++++++++ orchestrator/src/mcp/journal.ts | 58 ++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/orchestrator/src/__tests__/mcp-rpc.test.ts b/orchestrator/src/__tests__/mcp-rpc.test.ts index 719797368..bf50e12f9 100644 --- a/orchestrator/src/__tests__/mcp-rpc.test.ts +++ b/orchestrator/src/__tests__/mcp-rpc.test.ts @@ -77,6 +77,7 @@ function services(): McpServices { cancel: unavailable, resolveUnknown: unavailable, expire: unavailable, + expireUnadmitted: unavailable, }, }; } diff --git a/orchestrator/src/__tests__/mcp-store.test.ts b/orchestrator/src/__tests__/mcp-store.test.ts index 2146cd8f2..7a5d24bd0 100644 --- a/orchestrator/src/__tests__/mcp-store.test.ts +++ b/orchestrator/src/__tests__/mcp-store.test.ts @@ -515,3 +515,32 @@ test.skipIf(!reachable)( } }, ); + +test.skipIf(!reachable)( + "recovers calls after a crash before approval or admission is recorded", + async () => { + const f = await fixture(); + try { + const waiting = await f.journal.create(f.input, { kind: "approval" }, f.now); + const ready = await f.journal.create( + { ...f.input, toolCallId: "before-admission" }, + { + kind: "automation", + grant: { + upstreamName: tool.upstreamName, + automationWrite: true, + argumentConstraints: {}, + unrestricted: true, + }, + }, + f.now, + ); + const count = await f.journal.expireUnadmitted(new Date(f.now.getTime() + 61_000)); + expect(count).toBeGreaterThanOrEqual(2); + expect((await f.journal.get(waiting.id))!.state).toBe("canceled"); + expect((await f.journal.get(ready.id))!.state).toBe("canceled"); + } finally { + await f.cleanup(); + } + }, +); diff --git a/orchestrator/src/mcp/journal.ts b/orchestrator/src/mcp/journal.ts index 74c23a8c1..bd98b1a81 100644 --- a/orchestrator/src/mcp/journal.ts +++ b/orchestrator/src/mcp/journal.ts @@ -495,6 +495,64 @@ export function makeMcpJournal(db = getDb()) { return row; }); }, + /** Recover a crash between call insertion and its approval/admission transaction. */ + async expireUnadmitted(now: Date, limit = MCP_LIMITS.queueSize) { + if (!Number.isSafeInteger(limit) || limit < 1 || limit > MCP_LIMITS.queueSize) + throw new Error("invalid expiry batch size"); + const candidates = await db + .select({ id: execution.id }) + .from(execution) + .leftJoin(admission, eq(admission.executionId, execution.id)) + .where( + and( + inArray(execution.state, ["waiting_approval", "authorized"]), + lte(execution.deadline, now), + isNull(admission.executionId), + ), + ) + .orderBy(asc(execution.deadline)) + .limit(limit); + let expired = 0; + // One call lock per transaction avoids lock-order cycles with the lease sweeper. + for (const candidate of candidates) { + const changed = await db.transaction(async (tx) => { + const [call] = await tx + .select() + .from(execution) + .where(eq(execution.id, candidate.id)) + .for("update"); + if ( + !call || + !["waiting_approval", "authorized"].includes(call.state) || + call.deadline > now + ) + return false; + // Re-read after the row lock. Admission can have committed while this step waited. + const [lease] = await tx + .select() + .from(admission) + .where(eq(admission.executionId, call.id)); + if (lease) return false; + await tx + .update(execution) + .set({ state: "canceled", version: call.version + 1, updatedAt: now }) + .where(eq(execution.id, call.id)); + await tx + .update(approval) + .set({ decision: "expired", decidedAt: now }) + .where( + and( + eq(approval.executionId, call.id), + inArray(approval.decision, ["pending", "approved"]), + isNull(approval.consumedAt), + ), + ); + return true; + }); + if (changed) expired++; + } + return expired; + }, async expire(deploymentId: string, now: Date) { // Lock order is scope -> connection -> execution, shared with admission/dispatch. return db.transaction(async (tx) => { From 3763a1785ac7c4f591b2e5ed474d81c739e107f1 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 21:37:56 -0500 Subject: [PATCH 005/279] Preserve MCP queue deadlines and constraint document roots --- .../proto/engram/app/v1/profile.proto | 6 +- orchestrator/src/__tests__/mcp-domain.test.ts | 93 ++++++++++++++++++- orchestrator/src/__tests__/mcp-store.test.ts | 61 +++++++++++- orchestrator/src/__tests__/profiles.test.ts | 15 ++- .../src/gen/engram/app/v1/profile_pb.ts | 13 ++- orchestrator/src/mcp/bindings.ts | 20 ++-- orchestrator/src/mcp/journal.ts | 2 + orchestrator/src/mcp/types.ts | 15 ++- orchestrator/src/rpc/profiles.ts | 17 ++-- web/src/gen/engram/app/v1/profile_pb.ts | 13 ++- 10 files changed, 212 insertions(+), 43 deletions(-) diff --git a/crates/engram-protocol/proto/engram/app/v1/profile.proto b/crates/engram-protocol/proto/engram/app/v1/profile.proto index 0a5864120..04587a228 100644 --- a/crates/engram-protocol/proto/engram/app/v1/profile.proto +++ b/crates/engram-protocol/proto/engram/app/v1/profile.proto @@ -281,6 +281,10 @@ message DeleteProfileResponse {} message McpToolGrant { string upstream_name = 1; bool automation_write = 2; - string argument_constraints_json = 3; + reserved 3; + reserved "argument_constraints_json"; bool unrestricted = 4; + // Every document must pass in its own dialect and resource scope. + // An empty list requires an explicit unrestricted choice. + repeated string argument_constraint_documents_json = 5; } diff --git a/orchestrator/src/__tests__/mcp-domain.test.ts b/orchestrator/src/__tests__/mcp-domain.test.ts index cfafc10d2..ff4d676b9 100644 --- a/orchestrator/src/__tests__/mcp-domain.test.ts +++ b/orchestrator/src/__tests__/mcp-domain.test.ts @@ -32,7 +32,7 @@ const grant = { mcp: { upstreamName: tool.upstreamName, automationWrite: false, - argumentConstraints: {}, + argumentConstraintDocuments: [], unrestricted: true, }, }; @@ -72,6 +72,18 @@ describe("MCP domain", () => { validateMcpGrant({ ...grant, mcp: { ...grant.mcp, unrestricted: false } }), ).toThrow(); expect(() => validateMcpGrant({ ...grant, operation: tool.upstreamName })).toThrow(); + expect(() => + validateMcpGrant({ + ...grant, + mcp: { ...grant.mcp, unrestricted: false, argumentConstraintDocuments: [{}] }, + }), + ).toThrow(); + expect(() => + validateMcpGrant({ + ...grant, + mcp: { ...grant.mcp, argumentConstraintDocuments: [{ type: "object" }] }, + }), + ).toThrow(); }); test("catalog fingerprints bind upstream schema and explicit review", () => { const reviewed = reviewCatalog([tool]); @@ -123,13 +135,17 @@ test("MCP child grants intersect constraints and automation permission", async ( mcp: { ...grant.mcp, unrestricted: false, - argumentConstraints: { properties: { project: { const: "allowed" } }, required: ["project"] }, + argumentConstraintDocuments: [ + { properties: { project: { const: "allowed" } }, required: ["project"] }, + ], }, }; const child = { ...grant, mcp: { ...grant.mcp, automationWrite: true } }; const [effective] = intersectMcpGrants([parent], [child]); expect(effective!.mcp!.automationWrite).toBe(false); - expect(effective!.mcp!.argumentConstraints).toEqual(parent.mcp.argumentConstraints); + expect(effective!.mcp!.argumentConstraintDocuments).toEqual( + parent.mcp.argumentConstraintDocuments, + ); expect(() => intersectMcpGrants([], [child])).toThrow(); }); @@ -163,3 +179,74 @@ test("schema keyword checks do not reinterpret ordinary property names or consta assertSchemaShape({ properties: { child: { $ref: "https://example.test/schema" } } }), ).toThrow("local"); }); + +test("child intersections preserve independent local references with colliding definition names", async () => { + const { intersectMcpGrants } = await import("../mcp/bindings.ts"); + const parentSchema = { + $schema: "http://json-schema.org/draft-07/schema#", + $id: "https://schemas.example.test/constraint", + $defs: { item: { const: "allowed" } }, + type: "object", + properties: { project: { $ref: "#/$defs/item" } }, + required: ["project"], + }; + const childSchema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: "https://schemas.example.test/constraint", + $defs: { item: { type: "integer", minimum: 1, maximum: 5 } }, + type: "object", + properties: { count: { $ref: "#/$defs/item" } }, + required: ["count"], + }; + const parent = { + ...grant, + mcp: { ...grant.mcp, unrestricted: false, argumentConstraintDocuments: [parentSchema] }, + }; + const child = { + ...grant, + mcp: { ...grant.mcp, unrestricted: false, argumentConstraintDocuments: [childSchema] }, + }; + const [effective] = intersectMcpGrants([parent], [child]); + expect(effective!.mcp).toHaveProperty("argumentConstraintDocuments", [parentSchema, childSchema]); + expect(effective!.mcp!.unrestricted).toBe(false); + const documents = effective!.mcp!.argumentConstraintDocuments; + const resolve = (document: Record, pointer: string): unknown => + pointer + .slice(2) + .split("/") + .reduce((value, key) => { + if (!value || typeof value !== "object" || Array.isArray(value)) + throw new Error("missing reference target"); + return (value as Record)[key]; + }, document); + expect(resolve(documents[0]!, parentSchema.properties.project.$ref)).toEqual({ + const: "allowed", + }); + expect(resolve(documents[1]!, childSchema.properties.count.$ref)).toEqual({ + type: "integer", + minimum: 1, + maximum: 5, + }); + const [descendant] = intersectMcpGrants([effective!], [grant]); + expect(descendant!.mcp!.argumentConstraintDocuments).toEqual(documents); + const [binding] = compileBindings([ + { + id: "c1", + alias: "docs", + config, + state, + enabled: true, + tools: reviewCatalog([tool]), + grants: [effective!], + }, + ]); + expect(binding!.tools[0]!.grant.argumentConstraintDocuments).toEqual(documents); + parentSchema.$defs.item.const = "changed"; + childSchema.$defs.item.maximum = 100; + expect(resolve(documents[0]!, "#/$defs/item")).toEqual({ const: "allowed" }); + expect(resolve(documents[1]!, "#/$defs/item")).toEqual({ + type: "integer", + minimum: 1, + maximum: 5, + }); +}); diff --git a/orchestrator/src/__tests__/mcp-store.test.ts b/orchestrator/src/__tests__/mcp-store.test.ts index 7a5d24bd0..e6f44d521 100644 --- a/orchestrator/src/__tests__/mcp-store.test.ts +++ b/orchestrator/src/__tests__/mcp-store.test.ts @@ -186,7 +186,7 @@ describe("MCP durable authority", () => { grant: { upstreamName: tool.upstreamName, automationWrite: true, - argumentConstraints: {}, + argumentConstraintDocuments: [], unrestricted: true, }, }, @@ -271,7 +271,7 @@ describe("MCP authority isolation", () => { grant: { upstreamName: tool.upstreamName, automationWrite: false, - argumentConstraints: {}, + argumentConstraintDocuments: [], unrestricted: true, }, }, @@ -364,7 +364,7 @@ describe("MCP authority isolation", () => { grant: { upstreamName: tool.upstreamName, automationWrite: true, - argumentConstraints: {}, + argumentConstraintDocuments: [], unrestricted: true, }, }, @@ -444,7 +444,7 @@ test.skipIf(!reachable)("replicas share connection and deployment admission ceil grant: { upstreamName: tool.upstreamName, automationWrite: true, - argumentConstraints: {}, + argumentConstraintDocuments: [], unrestricted: true, }, }; @@ -529,7 +529,7 @@ test.skipIf(!reachable)( grant: { upstreamName: tool.upstreamName, automationWrite: true, - argumentConstraints: {}, + argumentConstraintDocuments: [], unrestricted: true, }, }, @@ -544,3 +544,54 @@ test.skipIf(!reachable)( } }, ); + +test.skipIf(!reachable)( + "admission retries preserve queue expiry and promotion starts a tool window", + async () => { + const f = await fixture(); + const authorization = { + kind: "automation" as const, + grant: { + upstreamName: tool.upstreamName, + automationWrite: true, + argumentConstraintDocuments: [], + unrestricted: true, + }, + }; + const at = (seconds: number) => new Date(f.now.getTime() + seconds * 1000); + try { + const calls = await Promise.all( + Array.from({ length: 6 }, (_, i) => + f.journal.create( + { ...f.input, toolCallId: `fixed-queue-${i}`, deadline: at(300) }, + authorization, + f.now, + ), + ), + ); + for (const call of calls.slice(0, 4)) + expect(await f.journal.admit(call.id, f.deploymentId, f.now)).toBe("admitted"); + for (const call of calls.slice(4)) { + expect(await f.journal.admit(call.id, f.deploymentId, f.now)).toBe("queued"); + expect(await f.journal.admit(call.id, f.deploymentId, at(20))).toBe("queued"); + const [lease] = await f.db + .select() + .from(mcpAdmission) + .where(eq(mcpAdmission.executionId, call.id)); + expect(lease!.expiresAt).toEqual(at(30)); + } + await f.journal.cancel(calls[0]!.id, "owner", false, at(25)); + expect(await f.journal.admit(calls[4]!.id, f.deploymentId, at(25))).toBe("admitted"); + const [promoted] = await f.db + .select() + .from(mcpAdmission) + .where(eq(mcpAdmission.executionId, calls[4]!.id)); + expect(promoted!.expiresAt).toEqual(at(85)); + await f.journal.expire(f.deploymentId, at(31)); + expect((await f.journal.get(calls[5]!.id))!.state).toBe("canceled"); + expect((await f.journal.get(calls[4]!.id))!.state).toBe("authorized"); + } finally { + await f.cleanup(); + } + }, +); diff --git a/orchestrator/src/__tests__/profiles.test.ts b/orchestrator/src/__tests__/profiles.test.ts index 9946fffdc..7694412ca 100644 --- a/orchestrator/src/__tests__/profiles.test.ts +++ b/orchestrator/src/__tests__/profiles.test.ts @@ -770,18 +770,27 @@ describe("user-scoped grants (ADR 0115)", () => { describe("MCP profile grant contract", () => { test("create, update, and list preserve explicit organization scope and constraints", async () => { const s = await spawn({ getSession: makeGetSession("admin", "admin"), store: makeFakeStore(), images: fakeImages(["img-1"]), mountCatalog: fakeCatalog([]) }); - const grant = { connectionId: "default-mcp", operation: "invoke", credentialScope: "org", resourceConstraints: [], mcp: { upstreamName: "Create.Record", automationWrite: true, argumentConstraintsJson: JSON.stringify({ type: "object", properties: { project: { const: "allowed" } }, required: ["project"] }), unrestricted: false } }; + const grant = { connectionId: "default-mcp", operation: "invoke", credentialScope: "org", resourceConstraints: [], mcp: { upstreamName: "Create.Record", automationWrite: true, argumentConstraintDocumentsJson: [ + JSON.stringify({ $schema: "http://json-schema.org/draft-07/schema#", $id: "https://schemas.example.test/constraint", $defs: { item: { const: "allowed" } }, type: "object", properties: { project: { $ref: "#/$defs/item" } }, required: ["project"] }), + JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://schemas.example.test/constraint", $defs: { item: { type: "integer", minimum: 1 } }, type: "object", properties: { count: { $ref: "#/$defs/item" } }, required: ["count"] }), + ], unrestricted: false } }; const input = { name: "Tools", imageId: "img-1", harness: "claude", integrationGrants: [grant] }; try { const created = await s.client.createProfile(input); expect(created.profile!.integrationGrants[0]!.credentialScope).toBe("org"); - expect(created.profile!.integrationGrants[0]!.mcp?.argumentConstraintsJson).toBe(grant.mcp.argumentConstraintsJson); + expect(created.profile!.integrationGrants[0]!.mcp?.argumentConstraintDocumentsJson).toEqual(grant.mcp.argumentConstraintDocumentsJson); const updated = await s.client.updateProfile({ ...input, id: created.profile!.id, name: "Renamed tools" }); expect(updated.profile!.integrationGrants[0]!.mcp?.automationWrite).toBe(true); + expect(updated.profile!.integrationGrants[0]!.mcp?.argumentConstraintDocumentsJson).toEqual(grant.mcp.argumentConstraintDocumentsJson); const listed = await s.client.listProfiles({}); expect(listed.profiles[0]!.integrationGrants[0]!.mcp?.upstreamName).toBe("Create.Record"); + expect(listed.profiles[0]!.integrationGrants[0]!.mcp?.argumentConstraintDocumentsJson).toEqual(grant.mcp.argumentConstraintDocumentsJson); await expectErr(s.client.updateProfile({ ...input, id: created.profile!.id, integrationGrants: [{ ...grant, credentialScope: "user" }] }), Code.InvalidArgument); - await expectErr(s.client.updateProfile({ ...input, id: created.profile!.id, integrationGrants: [{ ...grant, mcp: { ...grant.mcp, argumentConstraintsJson: "{}" } }] }), Code.InvalidArgument); + await expectErr(s.client.updateProfile({ ...input, id: created.profile!.id, integrationGrants: [{ ...grant, mcp: { ...grant.mcp, argumentConstraintDocumentsJson: [] } }] }), Code.InvalidArgument); + await expectErr(s.client.updateProfile({ ...input, id: created.profile!.id, integrationGrants: [{ ...grant, mcp: { ...grant.mcp, argumentConstraintDocumentsJson: ["[]"] } }] }), Code.InvalidArgument); + const unrestricted = await s.client.updateProfile({ ...input, id: created.profile!.id, integrationGrants: [{ ...grant, mcp: { ...grant.mcp, argumentConstraintDocumentsJson: [], unrestricted: true } }] }); + expect(unrestricted.profile!.integrationGrants[0]!.mcp?.argumentConstraintDocumentsJson).toEqual([]); + expect(unrestricted.profile!.integrationGrants[0]!.mcp?.unrestricted).toBe(true); } finally { await s.close(); } }); }); diff --git a/orchestrator/src/gen/engram/app/v1/profile_pb.ts b/orchestrator/src/gen/engram/app/v1/profile_pb.ts index d939bc0ca..84555fcb6 100644 --- a/orchestrator/src/gen/engram/app/v1/profile_pb.ts +++ b/orchestrator/src/gen/engram/app/v1/profile_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/profile.proto. */ export const file_engram_app_v1_profile: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8ipQEKF1Byb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EhUKDWNvbm5lY3Rpb25faWQYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEhwKFHJlc291cmNlX2NvbnN0cmFpbnRzGAMgAygJEhgKEGNyZWRlbnRpYWxfc2NvcGUYBCABKAkSKAoDbWNwGAUgASgLMhsuZW5ncmFtLmFwcC52MS5NY3BUb29sR3JhbnQiUwoOUHJvZmlsZU5ldHdvcmsSDwoHZGVmYXVsdBgBIAEoCRITCgthbGxvd19ob3N0cxgCIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAMgAygJIm0KDVByb2ZpbGVTZWNyZXQSCwoDcmVmGAEgASgJEg8KB2Vudl92YXIYAiABKAkSDAoEbW9kZRgDIAEoCRITCgthbGxvd19ob3N0cxgEIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAUgAygJIm4KD1Byb2ZpbGVTbmFwc2hvdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGljb24YAyABKAkSEAoIYXJjaGl2ZWQYBCABKAgSEQoJaW1hZ2VfdXJpGAUgASgJEg4KBnNraWxscxgGIAMoCSJLChNMaXN0UHJvZmlsZXNSZXF1ZXN0EhgKEGluY2x1ZGVfYXJjaGl2ZWQYASABKAgSGgoSZm9yX3Nlc3Npb25fcGlja2VyGAIgASgIIkAKFExpc3RQcm9maWxlc1Jlc3BvbnNlEigKCHByb2ZpbGVzGAEgAygLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIh8KEUdldFByb2ZpbGVSZXF1ZXN0EgoKAmlkGAEgASgJIj0KEkdldFByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIrAFChRDcmVhdGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEgwKBGljb24YAyABKAkSEAoIaW1hZ2VfaWQYBCABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgFIAEoCBJCCghlbnZfdmFycxgGIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgHIAMoCRIuCgduZXR3b3JrGAkgASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAogAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDCABKAlIAIgBARISCgVtb2RlbBgNIAEoCUgBiAEBEhMKBmVmZm9ydBgOIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgRIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEiADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgTIAEoCUgDiAEBEicKBGFwcHMYFCADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASGQoRcHJvZ3JhbW1hdGljX29ubHkYFSABKAgaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSLXBQoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlKBAgJEApKBAgMEA1KBAgQEBFKBAgREBIiQAoVVXBkYXRlUHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiIgoURGVsZXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlUHJvZmlsZVJlc3BvbnNlIngKDE1jcFRvb2xHcmFudBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhgKEGF1dG9tYXRpb25fd3JpdGUYAiABKAgSIQoZYXJndW1lbnRfY29uc3RyYWludHNfanNvbhgDIAEoCRIUCgx1bnJlc3RyaWN0ZWQYBCABKAgywQQKDlByb2ZpbGVTZXJ2aWNlElcKDExpc3RQcm9maWxlcxIiLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBojLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USUQoKR2V0UHJvZmlsZRIgLmVuZ3JhbS5hcHAudjEuR2V0UHJvZmlsZVJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXNwb25zZRJaCg1DcmVhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlc3BvbnNlEloKDVVwZGF0ZVByb2ZpbGUSIy5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVzcG9uc2USbwoURGlzY292ZXJQcm9maWxlUmVwb3MSKi5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRJaCg1EZWxldGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5EZWxldGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlc3BvbnNlYgZwcm90bzM"); + fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8ipQEKF1Byb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EhUKDWNvbm5lY3Rpb25faWQYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEhwKFHJlc291cmNlX2NvbnN0cmFpbnRzGAMgAygJEhgKEGNyZWRlbnRpYWxfc2NvcGUYBCABKAkSKAoDbWNwGAUgASgLMhsuZW5ncmFtLmFwcC52MS5NY3BUb29sR3JhbnQiUwoOUHJvZmlsZU5ldHdvcmsSDwoHZGVmYXVsdBgBIAEoCRITCgthbGxvd19ob3N0cxgCIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAMgAygJIm0KDVByb2ZpbGVTZWNyZXQSCwoDcmVmGAEgASgJEg8KB2Vudl92YXIYAiABKAkSDAoEbW9kZRgDIAEoCRITCgthbGxvd19ob3N0cxgEIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAUgAygJIm4KD1Byb2ZpbGVTbmFwc2hvdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGljb24YAyABKAkSEAoIYXJjaGl2ZWQYBCABKAgSEQoJaW1hZ2VfdXJpGAUgASgJEg4KBnNraWxscxgGIAMoCSJLChNMaXN0UHJvZmlsZXNSZXF1ZXN0EhgKEGluY2x1ZGVfYXJjaGl2ZWQYASABKAgSGgoSZm9yX3Nlc3Npb25fcGlja2VyGAIgASgIIkAKFExpc3RQcm9maWxlc1Jlc3BvbnNlEigKCHByb2ZpbGVzGAEgAygLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIh8KEUdldFByb2ZpbGVSZXF1ZXN0EgoKAmlkGAEgASgJIj0KEkdldFByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIrAFChRDcmVhdGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEgwKBGljb24YAyABKAkSEAoIaW1hZ2VfaWQYBCABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgFIAEoCBJCCghlbnZfdmFycxgGIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgHIAMoCRIuCgduZXR3b3JrGAkgASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAogAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDCABKAlIAIgBARISCgVtb2RlbBgNIAEoCUgBiAEBEhMKBmVmZm9ydBgOIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgRIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEiADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgTIAEoCUgDiAEBEicKBGFwcHMYFCADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASGQoRcHJvZ3JhbW1hdGljX29ubHkYFSABKAgaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSLXBQoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlKBAgJEApKBAgMEA1KBAgQEBFKBAgREBIiQAoVVXBkYXRlUHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiIgoURGVsZXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlUHJvZmlsZVJlc3BvbnNlIqIBCgxNY3BUb29sR3JhbnQSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRIYChBhdXRvbWF0aW9uX3dyaXRlGAIgASgIEhQKDHVucmVzdHJpY3RlZBgEIAEoCBIqCiJhcmd1bWVudF9jb25zdHJhaW50X2RvY3VtZW50c19qc29uGAUgAygJSgQIAxAEUhlhcmd1bWVudF9jb25zdHJhaW50c19qc29uMsEECg5Qcm9maWxlU2VydmljZRJXCgxMaXN0UHJvZmlsZXMSIi5lbmdyYW0uYXBwLnYxLkxpc3RQcm9maWxlc1JlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkxpc3RQcm9maWxlc1Jlc3BvbnNlElEKCkdldFByb2ZpbGUSIC5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5HZXRQcm9maWxlUmVzcG9uc2USWgoNQ3JlYXRlUHJvZmlsZRIjLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVByb2ZpbGVSZXNwb25zZRJaCg1VcGRhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVXBkYXRlUHJvZmlsZVJlc3BvbnNlEm8KFERpc2NvdmVyUHJvZmlsZVJlcG9zEiouZW5ncmFtLmFwcC52MS5EaXNjb3ZlclByb2ZpbGVSZXBvc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVzcG9uc2USWgoNRGVsZXRlUHJvZmlsZRIjLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkRlbGV0ZVByb2ZpbGVSZXNwb25zZWIGcHJvdG8z"); /** * An admin-curated bundle of session-launch inputs (ADR §1). @@ -923,14 +923,17 @@ export type McpToolGrant = Message<"engram.app.v1.McpToolGrant"> & { automationWrite: boolean; /** - * @generated from field: string argument_constraints_json = 3; + * @generated from field: bool unrestricted = 4; */ - argumentConstraintsJson: string; + unrestricted: boolean; /** - * @generated from field: bool unrestricted = 4; + * Every document must pass in its own dialect and resource scope. + * An empty list requires an explicit unrestricted choice. + * + * @generated from field: repeated string argument_constraint_documents_json = 5; */ - unrestricted: boolean; + argumentConstraintDocumentsJson: string[]; }; /** diff --git a/orchestrator/src/mcp/bindings.ts b/orchestrator/src/mcp/bindings.ts index 4d15f9ed7..55123fd87 100644 --- a/orchestrator/src/mcp/bindings.ts +++ b/orchestrator/src/mcp/bindings.ts @@ -94,25 +94,23 @@ export function intersectMcpGrants( ); if (!inherited) throw new Error("child tool grant exceeds parent authority"); validateMcpGrant(inherited); - const parentConstraint = inherited.mcp!.argumentConstraints; - const childConstraint = child.mcp!.argumentConstraints; const unrestricted = inherited.mcp!.unrestricted && child.mcp!.unrestricted; - const argumentConstraints = unrestricted - ? {} - : inherited.mcp!.unrestricted - ? childConstraint - : child.mcp!.unrestricted - ? parentConstraint - : { allOf: [parentConstraint, childConstraint] }; - return structuredClone({ + // Keep each root intact: allOf wrapping changes local references and resource scope. + const argumentConstraintDocuments = [ + ...inherited.mcp!.argumentConstraintDocuments, + ...child.mcp!.argumentConstraintDocuments, + ]; + const effective = structuredClone({ ...child, mcp: { ...child.mcp!, automationWrite: inherited.mcp!.automationWrite && child.mcp!.automationWrite, unrestricted, - argumentConstraints, + argumentConstraintDocuments, }, }); + validateMcpGrant(effective); + return effective; }); } diff --git a/orchestrator/src/mcp/journal.ts b/orchestrator/src/mcp/journal.ts index bd98b1a81..c7bac4d08 100644 --- a/orchestrator/src/mcp/journal.ts +++ b/orchestrator/src/mcp/journal.ts @@ -268,6 +268,8 @@ export function makeMcpJournal(db = getDb()) { MCP_LIMITS.connectionConcurrency && active.filter((a) => a.sessionId === call.sessionId).length < MCP_LIMITS.sessionConcurrency; + // Polling cannot extend the original queue deadline. Promotion starts a tool lease. + if (existing && !available) return "queued"; const state = available ? "admitted" : "queued"; if ( !available && diff --git a/orchestrator/src/mcp/types.ts b/orchestrator/src/mcp/types.ts index af8a3f9a1..8c981bcb3 100644 --- a/orchestrator/src/mcp/types.ts +++ b/orchestrator/src/mcp/types.ts @@ -1,11 +1,14 @@ import type { ProfileIntegrationGrant } from "../db/schema.ts"; import type { McpConfig } from "./config.ts"; import { assertSchemaShape } from "./catalog.ts"; +import { canonicalJson } from "./hash.ts"; +import { MCP_LIMITS } from "./limits.ts"; export interface McpToolGrant { upstreamName: string; automationWrite: boolean; - argumentConstraints: Record; + /** Conjunction of independent documents. Compile each in its own dialect/resource scope. */ + argumentConstraintDocuments: Record[]; /** Required affirmative choice when constraints are empty. */ unrestricted: boolean; } @@ -66,8 +69,14 @@ export function validateMcpGrant(grant: ProfileIntegrationGrant): void { typeof tool.unrestricted !== "boolean" ) throw new Error("invalid MCP tool grant"); - assertSchemaShape(tool.argumentConstraints); - const empty = Object.keys(tool.argumentConstraints).length === 0; + const documents = tool.argumentConstraintDocuments; + if (!Array.isArray(documents)) throw new Error("MCP constraints must be schema documents"); + if (Buffer.byteLength(canonicalJson(documents)) > MCP_LIMITS.argumentBytes) + throw new Error("MCP constraints are too large"); + for (const document of documents) assertSchemaShape(document); + const empty = documents.length === 0; + if (!empty && documents.every((document) => Object.keys(document).length === 0)) + throw new Error("empty constraints require an explicit unrestricted choice"); if (empty !== tool.unrestricted) throw new Error("empty constraints require an explicit unrestricted choice"); } diff --git a/orchestrator/src/rpc/profiles.ts b/orchestrator/src/rpc/profiles.ts index f7f337b20..9d3d55590 100644 --- a/orchestrator/src/rpc/profiles.ts +++ b/orchestrator/src/rpc/profiles.ts @@ -135,7 +135,7 @@ function toProto(row: ProfileRow, isAdmin: boolean): Profile { mcp: grant.mcp ? { upstreamName: grant.mcp.upstreamName, automationWrite: grant.mcp.automationWrite, - argumentConstraintsJson: JSON.stringify(grant.mcp.argumentConstraints), + argumentConstraintDocumentsJson: grant.mcp.argumentConstraintDocuments.map((document) => JSON.stringify(document)), unrestricted: grant.mcp.unrestricted, } : undefined, })), @@ -492,7 +492,7 @@ export function registerProfiles(router: ConnectRouter, deps?: ProfileDeps): voi operation?: string; resourceConstraints?: string[]; credentialScope?: string; - mcp?: { upstreamName: string; automationWrite: boolean; argumentConstraintsJson: string; unrestricted: boolean }; + mcp?: { upstreamName: string; automationWrite: boolean; argumentConstraintDocumentsJson: string[]; unrestricted: boolean }; }>, ): ProfileIntegrationGrant[] { return grants.map((grant) => { @@ -505,11 +505,14 @@ export function registerProfiles(router: ConnectRouter, deps?: ProfileDeps): voi } let mcp: ProfileIntegrationGrant["mcp"]; if (grant.mcp) { - let constraints: unknown; - try { constraints = JSON.parse(grant.mcp.argumentConstraintsJson); } - catch { throw new ConnectError("MCP constraints must be valid JSON", Code.InvalidArgument); } - if (!constraints || typeof constraints !== "object" || Array.isArray(constraints)) throw new ConnectError("MCP constraints must be a JSON object", Code.InvalidArgument); - mcp = { upstreamName: grant.mcp.upstreamName, automationWrite: grant.mcp.automationWrite, argumentConstraints: constraints as Record, unrestricted: grant.mcp.unrestricted }; + const documents = grant.mcp.argumentConstraintDocumentsJson.map((json) => { + let document: unknown; + try { document = JSON.parse(json); } + catch { throw new ConnectError("MCP constraints must be valid JSON", Code.InvalidArgument); } + if (!document || typeof document !== "object" || Array.isArray(document)) throw new ConnectError("MCP constraints must be JSON objects", Code.InvalidArgument); + return document as Record; + }); + mcp = { upstreamName: grant.mcp.upstreamName, automationWrite: grant.mcp.automationWrite, argumentConstraintDocuments: documents, unrestricted: grant.mcp.unrestricted }; } return { ...(mcp ? { mcp } : {}), diff --git a/web/src/gen/engram/app/v1/profile_pb.ts b/web/src/gen/engram/app/v1/profile_pb.ts index d939bc0ca..84555fcb6 100644 --- a/web/src/gen/engram/app/v1/profile_pb.ts +++ b/web/src/gen/engram/app/v1/profile_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/profile.proto. */ export const file_engram_app_v1_profile: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8ipQEKF1Byb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EhUKDWNvbm5lY3Rpb25faWQYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEhwKFHJlc291cmNlX2NvbnN0cmFpbnRzGAMgAygJEhgKEGNyZWRlbnRpYWxfc2NvcGUYBCABKAkSKAoDbWNwGAUgASgLMhsuZW5ncmFtLmFwcC52MS5NY3BUb29sR3JhbnQiUwoOUHJvZmlsZU5ldHdvcmsSDwoHZGVmYXVsdBgBIAEoCRITCgthbGxvd19ob3N0cxgCIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAMgAygJIm0KDVByb2ZpbGVTZWNyZXQSCwoDcmVmGAEgASgJEg8KB2Vudl92YXIYAiABKAkSDAoEbW9kZRgDIAEoCRITCgthbGxvd19ob3N0cxgEIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAUgAygJIm4KD1Byb2ZpbGVTbmFwc2hvdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGljb24YAyABKAkSEAoIYXJjaGl2ZWQYBCABKAgSEQoJaW1hZ2VfdXJpGAUgASgJEg4KBnNraWxscxgGIAMoCSJLChNMaXN0UHJvZmlsZXNSZXF1ZXN0EhgKEGluY2x1ZGVfYXJjaGl2ZWQYASABKAgSGgoSZm9yX3Nlc3Npb25fcGlja2VyGAIgASgIIkAKFExpc3RQcm9maWxlc1Jlc3BvbnNlEigKCHByb2ZpbGVzGAEgAygLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIh8KEUdldFByb2ZpbGVSZXF1ZXN0EgoKAmlkGAEgASgJIj0KEkdldFByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIrAFChRDcmVhdGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEgwKBGljb24YAyABKAkSEAoIaW1hZ2VfaWQYBCABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgFIAEoCBJCCghlbnZfdmFycxgGIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgHIAMoCRIuCgduZXR3b3JrGAkgASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAogAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDCABKAlIAIgBARISCgVtb2RlbBgNIAEoCUgBiAEBEhMKBmVmZm9ydBgOIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgRIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEiADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgTIAEoCUgDiAEBEicKBGFwcHMYFCADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASGQoRcHJvZ3JhbW1hdGljX29ubHkYFSABKAgaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSLXBQoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlKBAgJEApKBAgMEA1KBAgQEBFKBAgREBIiQAoVVXBkYXRlUHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiIgoURGVsZXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlUHJvZmlsZVJlc3BvbnNlIngKDE1jcFRvb2xHcmFudBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhgKEGF1dG9tYXRpb25fd3JpdGUYAiABKAgSIQoZYXJndW1lbnRfY29uc3RyYWludHNfanNvbhgDIAEoCRIUCgx1bnJlc3RyaWN0ZWQYBCABKAgywQQKDlByb2ZpbGVTZXJ2aWNlElcKDExpc3RQcm9maWxlcxIiLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBojLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USUQoKR2V0UHJvZmlsZRIgLmVuZ3JhbS5hcHAudjEuR2V0UHJvZmlsZVJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXNwb25zZRJaCg1DcmVhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlc3BvbnNlEloKDVVwZGF0ZVByb2ZpbGUSIy5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVzcG9uc2USbwoURGlzY292ZXJQcm9maWxlUmVwb3MSKi5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRJaCg1EZWxldGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5EZWxldGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlc3BvbnNlYgZwcm90bzM"); + fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8ipQEKF1Byb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EhUKDWNvbm5lY3Rpb25faWQYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEhwKFHJlc291cmNlX2NvbnN0cmFpbnRzGAMgAygJEhgKEGNyZWRlbnRpYWxfc2NvcGUYBCABKAkSKAoDbWNwGAUgASgLMhsuZW5ncmFtLmFwcC52MS5NY3BUb29sR3JhbnQiUwoOUHJvZmlsZU5ldHdvcmsSDwoHZGVmYXVsdBgBIAEoCRITCgthbGxvd19ob3N0cxgCIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAMgAygJIm0KDVByb2ZpbGVTZWNyZXQSCwoDcmVmGAEgASgJEg8KB2Vudl92YXIYAiABKAkSDAoEbW9kZRgDIAEoCRITCgthbGxvd19ob3N0cxgEIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAUgAygJIm4KD1Byb2ZpbGVTbmFwc2hvdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGljb24YAyABKAkSEAoIYXJjaGl2ZWQYBCABKAgSEQoJaW1hZ2VfdXJpGAUgASgJEg4KBnNraWxscxgGIAMoCSJLChNMaXN0UHJvZmlsZXNSZXF1ZXN0EhgKEGluY2x1ZGVfYXJjaGl2ZWQYASABKAgSGgoSZm9yX3Nlc3Npb25fcGlja2VyGAIgASgIIkAKFExpc3RQcm9maWxlc1Jlc3BvbnNlEigKCHByb2ZpbGVzGAEgAygLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIh8KEUdldFByb2ZpbGVSZXF1ZXN0EgoKAmlkGAEgASgJIj0KEkdldFByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIrAFChRDcmVhdGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEgwKBGljb24YAyABKAkSEAoIaW1hZ2VfaWQYBCABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgFIAEoCBJCCghlbnZfdmFycxgGIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgHIAMoCRIuCgduZXR3b3JrGAkgASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAogAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDCABKAlIAIgBARISCgVtb2RlbBgNIAEoCUgBiAEBEhMKBmVmZm9ydBgOIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgRIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEiADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgTIAEoCUgDiAEBEicKBGFwcHMYFCADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASGQoRcHJvZ3JhbW1hdGljX29ubHkYFSABKAgaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSLXBQoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlKBAgJEApKBAgMEA1KBAgQEBFKBAgREBIiQAoVVXBkYXRlUHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiIgoURGVsZXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlUHJvZmlsZVJlc3BvbnNlIqIBCgxNY3BUb29sR3JhbnQSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRIYChBhdXRvbWF0aW9uX3dyaXRlGAIgASgIEhQKDHVucmVzdHJpY3RlZBgEIAEoCBIqCiJhcmd1bWVudF9jb25zdHJhaW50X2RvY3VtZW50c19qc29uGAUgAygJSgQIAxAEUhlhcmd1bWVudF9jb25zdHJhaW50c19qc29uMsEECg5Qcm9maWxlU2VydmljZRJXCgxMaXN0UHJvZmlsZXMSIi5lbmdyYW0uYXBwLnYxLkxpc3RQcm9maWxlc1JlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkxpc3RQcm9maWxlc1Jlc3BvbnNlElEKCkdldFByb2ZpbGUSIC5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5HZXRQcm9maWxlUmVzcG9uc2USWgoNQ3JlYXRlUHJvZmlsZRIjLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVByb2ZpbGVSZXNwb25zZRJaCg1VcGRhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVXBkYXRlUHJvZmlsZVJlc3BvbnNlEm8KFERpc2NvdmVyUHJvZmlsZVJlcG9zEiouZW5ncmFtLmFwcC52MS5EaXNjb3ZlclByb2ZpbGVSZXBvc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVzcG9uc2USWgoNRGVsZXRlUHJvZmlsZRIjLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkRlbGV0ZVByb2ZpbGVSZXNwb25zZWIGcHJvdG8z"); /** * An admin-curated bundle of session-launch inputs (ADR §1). @@ -923,14 +923,17 @@ export type McpToolGrant = Message<"engram.app.v1.McpToolGrant"> & { automationWrite: boolean; /** - * @generated from field: string argument_constraints_json = 3; + * @generated from field: bool unrestricted = 4; */ - argumentConstraintsJson: string; + unrestricted: boolean; /** - * @generated from field: bool unrestricted = 4; + * Every document must pass in its own dialect and resource scope. + * An empty list requires an explicit unrestricted choice. + * + * @generated from field: repeated string argument_constraint_documents_json = 5; */ - unrestricted: boolean; + argumentConstraintDocumentsJson: string[]; }; /** From c93c421830fc5a63efc359c4627d80b4c601edc3 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 22:25:54 -0500 Subject: [PATCH 006/279] Add restricted MCP worker with bound intents and zone isolation --- deploy/helm/engram/templates/mcp-worker.yaml | 176 +++++++++ .../engram/values-mcp-worker.yaml.example | 39 ++ deploy/helm/engram/values.yaml | 12 + docker/mcp-worker.Dockerfile | 19 + mcp-worker/README.md | 169 +++++++-- mcp-worker/package-lock.json | 37 +- mcp-worker/package.json | 6 +- mcp-worker/src/contracts.ts | 140 ++++++++ mcp-worker/src/deployment.ts | 69 ++++ mcp-worker/src/encoding.ts | 29 ++ mcp-worker/src/engine.ts | 339 ++++++++++++++++++ mcp-worker/src/index.ts | 6 +- mcp-worker/src/main.ts | 98 +++++ mcp-worker/src/network.ts | 331 +++++++++++++++++ mcp-worker/src/schema-child.ts | 100 ++++++ mcp-worker/src/schema.ts | 63 ++++ mcp-worker/src/service.ts | 279 ++++++++++++++ mcp-worker/src/validation.ts | 153 ++++++++ mcp-worker/test/contracts.test.ts | 74 ++++ mcp-worker/test/deployment.test.ts | 175 +++++++++ mcp-worker/test/engine.test.ts | 316 ++++++++++++++++ mcp-worker/test/fixtures/worker-tls.ts | 77 ++++ mcp-worker/test/network.test.ts | 196 ++++++++++ mcp-worker/test/schema.test.ts | 146 ++++++++ mcp-worker/test/service.test.ts | 256 +++++++++++++ 25 files changed, 3267 insertions(+), 38 deletions(-) create mode 100644 deploy/helm/engram/templates/mcp-worker.yaml create mode 100644 deploy/helm/engram/values-mcp-worker.yaml.example create mode 100644 docker/mcp-worker.Dockerfile create mode 100644 mcp-worker/src/contracts.ts create mode 100644 mcp-worker/src/deployment.ts create mode 100644 mcp-worker/src/encoding.ts create mode 100644 mcp-worker/src/engine.ts create mode 100644 mcp-worker/src/main.ts create mode 100644 mcp-worker/src/network.ts create mode 100644 mcp-worker/src/schema-child.ts create mode 100644 mcp-worker/src/schema.ts create mode 100644 mcp-worker/src/service.ts create mode 100644 mcp-worker/src/validation.ts create mode 100644 mcp-worker/test/contracts.test.ts create mode 100644 mcp-worker/test/deployment.test.ts create mode 100644 mcp-worker/test/engine.test.ts create mode 100644 mcp-worker/test/fixtures/worker-tls.ts create mode 100644 mcp-worker/test/network.test.ts create mode 100644 mcp-worker/test/schema.test.ts create mode 100644 mcp-worker/test/service.test.ts diff --git a/deploy/helm/engram/templates/mcp-worker.yaml b/deploy/helm/engram/templates/mcp-worker.yaml new file mode 100644 index 000000000..2948a34a6 --- /dev/null +++ b/deploy/helm/engram/templates/mcp-worker.yaml @@ -0,0 +1,176 @@ +{{- if .Values.mcpWorker.enabled }} +{{- range $zone := .Values.mcpWorker.zones }} +{{- if not $zone.isolationConfirmed }}{{ fail "MCP worker namespace isolation must be verified" }}{{- end }} +{{- $name := printf "%s-mcp-%s" (include "engram.fullname" $) $zone.policy.id | trunc 63 | trimSuffix "-" }} +{{- $namespace := required "MCP worker requires a dedicated namespace" $zone.namespace }} +{{- if eq $namespace $.Release.Namespace }}{{ fail "MCP worker namespace must differ from the application namespace" }}{{- end }} +{{- $tls := required "MCP worker TLS secret is required" $zone.tlsSecret }} +{{- $rules := $zone.egressRules }} +{{- if eq $zone.policy.mode "public" }} +{{- $deny4 := list "0.0.0.0/8" "10.0.0.0/8" "100.64.0.0/10" "127.0.0.0/8" "169.254.0.0/16" "172.16.0.0/12" "192.0.0.0/24" "192.0.2.0/24" "192.88.99.0/24" "192.168.0.0/16" "198.18.0.0/15" "198.51.100.0/24" "203.0.113.0/24" "224.0.0.0/4" "240.0.0.0/4" "168.63.129.16/32" }} +{{- $deny6 := list "2001::/23" "2001:db8::/32" "2002::/16" "3fff::/20" }} +{{- range $deny := $zone.policy.denyCidrs }} +{{- if not (contains ":" $deny) }}{{- $deny4 = append $deny4 $deny }} +{{- else if regexMatch "^[23]" $deny }}{{- $deny6 = append $deny6 $deny }}{{- end }} +{{- end }} +{{- $rules = list (dict "cidr" "0.0.0.0/0" "except" ($deny4 | uniq)) (dict "cidr" "2000::/3" "except" ($deny6 | uniq)) }} +{{- end }} +{{- if not $rules }}{{ fail "MCP worker egress rules are required" }}{{- end }} +{{- $callers := required "MCP worker caller certificate pins are required" $zone.callers }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ $name }} + namespace: {{ $namespace }} +automountServiceAccountToken: false +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $name }} + namespace: {{ $namespace }} +data: + config.json: {{ dict "zone" $zone.policy "callers" $callers "egressRules" $rules | toJson | quote }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + namespace: {{ $namespace }} +spec: + replicas: {{ default 1 $zone.replicas }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ $.Release.Name }} + app.kubernetes.io/component: mcp-worker + engrams.io/mcp-zone: {{ $zone.policy.id }} + template: + metadata: + labels: + app.kubernetes.io/instance: {{ $.Release.Name }} + app.kubernetes.io/component: mcp-worker + engrams.io/mcp-zone: {{ $zone.policy.id }} + annotations: + checksum/zone-policy: {{ dict "zone" $zone.policy "callers" $callers "egressRules" $rules | toJson | sha256sum }} + spec: + serviceAccountName: {{ $name }} + automountServiceAccountToken: false + enableServiceLinks: false + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: worker + image: "{{ $.Values.mcpWorker.image.repository }}:{{ default $.Chart.AppVersion $.Values.mcpWorker.image.tag }}" + imagePullPolicy: {{ $.Values.mcpWorker.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] + env: + - name: MCP_WORKER_ENABLED + value: {{ default false $zone.dispatchEnabled | quote }} + ports: + - name: rpc + containerPort: 8443 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "2" + memory: 1Gi + volumeMounts: + - name: config + mountPath: /etc/mcp-worker + readOnly: true + - name: tls + mountPath: /etc/mcp-worker/tls + readOnly: true + volumes: + - name: config + configMap: + name: {{ $name }} + - name: tls + secret: + secretName: {{ $tls }} + defaultMode: 0440 +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + namespace: {{ $namespace }} +spec: + type: ClusterIP + selector: + app.kubernetes.io/instance: {{ $.Release.Name }} + app.kubernetes.io/component: mcp-worker + engrams.io/mcp-zone: {{ $zone.policy.id }} + ports: + - name: rpc + port: 8443 + targetPort: rpc +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ $name }} + namespace: {{ $namespace }} +spec: + podSelector: + matchLabels: + app.kubernetes.io/instance: {{ $.Release.Name }} + app.kubernetes.io/component: mcp-worker + engrams.io/mcp-zone: {{ $zone.policy.id }} + policyTypes: [Ingress, Egress] + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ $.Release.Namespace }} + podSelector: + matchLabels: + app.kubernetes.io/instance: {{ $.Release.Name }} + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: [coordinator, orchestrator] + ports: + - protocol: TCP + port: 8443 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ required "DNS namespace is required" $zone.dns.namespace }} + podSelector: + matchLabels: + {{- required "DNS pod selector is required" $zone.dns.podLabels | toYaml | nindent 14 }} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + {{- range $rule := $rules }} + - ipBlock: + cidr: {{ $rule.cidr }} + {{- with $rule.except }} + except: + {{- toYaml . | nindent 14 }} + {{- end }} + {{- end }} + ports: + {{- range $port := required "Approved HTTPS ports are required" $zone.ports }} + - protocol: TCP + port: {{ $port }} + {{- end }} +{{- end }} +{{- end }} diff --git a/deploy/helm/engram/values-mcp-worker.yaml.example b/deploy/helm/engram/values-mcp-worker.yaml.example new file mode 100644 index 000000000..c47982ba2 --- /dev/null +++ b/deploy/helm/engram/values-mcp-worker.yaml.example @@ -0,0 +1,39 @@ +# This example is disabled. Create the namespace and TLS Secret separately. +# The Secret contains tls.key, tls.crt, and the trusted caller CA in ca.crt. +# Set real SHA-256 client certificate fingerprints. Give the coordinator token +# access and the orchestrator tool access with different client certificates. +mcpWorker: + enabled: false + zones: + - namespace: mcp-tools + isolationConfirmed: false + dispatchEnabled: false + replicas: 1 + tlsSecret: mcp-worker-rpc-tls + callers: + - fingerprint: "REPLACE_WITH_ORCHESTRATOR_CERTIFICATE_SHA256" + role: orchestrator + - fingerprint: "REPLACE_WITH_COORDINATOR_CERTIFICATE_SHA256" + role: coordinator + policy: + id: private-tools + revision: "1" + mode: internal + authorities: [tools.example] + aliases: [] + cidrs: [10.20.0.0/16] + # Include every platform administration network, including API servers. + denyCidrs: [10.20.0.0/24] + # Optional private provider CA PEM. This is not the RPC caller CA. + # ca: | + # -----BEGIN CERTIFICATE----- + # ... + limits: {execute: 32, discover: 2, oauth: 4, schema: 4, toolMs: 60000} + egressRules: + - cidr: 10.20.0.0/16 + except: [10.20.0.0/24] + ports: [443] + dns: + namespace: kube-system + podLabels: + k8s-app: kube-dns diff --git a/deploy/helm/engram/values.yaml b/deploy/helm/engram/values.yaml index c1463c673..632726066 100644 --- a/deploy/helm/engram/values.yaml +++ b/deploy/helm/engram/values.yaml @@ -677,3 +677,15 @@ orchestrator: nodeSelector: {} tolerations: [] affinity: {} + +# Isolated external MCP workers. Only operators can edit these zone policies. +# Provision a separate namespace with enforced NetworkPolicy before enabling. +# Namespace policies are additive: no other policy may grant broader egress. +mcpWorker: + enabled: false + image: + repository: ghcr.io/cortexapps/engrams/mcp-worker + tag: "" + pullPolicy: IfNotPresent + zones: [] + # See values-mcp-worker.yaml.example for a complete, disabled zone. diff --git a/docker/mcp-worker.Dockerfile b/docker/mcp-worker.Dockerfile new file mode 100644 index 000000000..f4d865a52 --- /dev/null +++ b/docker/mcp-worker.Dockerfile @@ -0,0 +1,19 @@ +# The official Node 24 LTS release is pinned in both stages. +FROM node:24.22.2-bookworm-slim AS build +WORKDIR /app +COPY mcp-worker/package.json mcp-worker/package-lock.json ./ +RUN npm ci --ignore-scripts +COPY mcp-worker/tsconfig.json mcp-worker/tsconfig.build.json ./ +COPY mcp-worker/src ./src +RUN npm run build && npm prune --omit=dev --ignore-scripts + +FROM node:24.22.2-bookworm-slim +ENV NODE_ENV=production +WORKDIR /app +COPY --from=build --chown=node:node /app/package.json ./ +COPY --from=build --chown=node:node /app/node_modules ./node_modules +COPY --from=build --chown=node:node /app/dist ./dist +USER node +EXPOSE 8443 +# This is inherited by disposable schema children. No core or heap dump is kept. +ENTRYPOINT ["/bin/sh", "-c", "ulimit -c 0; exec node --max-old-space-size=256 dist/main.js"] diff --git a/mcp-worker/README.md b/mcp-worker/README.md index acb9d7be4..6aa66d371 100644 --- a/mcp-worker/README.md +++ b/mcp-worker/README.md @@ -1,12 +1,8 @@ -# MCP worker foundation +# Restricted MCP worker -This is a standalone Node 24 TypeScript ESM package. The official client is pinned -to 2.4.0. Ajv 8.20.0 and ipaddr.js are pinned for the worker validation boundary. -`package-lock.json` records exact dependency versions and integrity hashes. - -There is no production service entrypoint or worker policy in this foundation. -The empty source module reserves the package build boundary. Later changes add -fixed authenticated RPCs and network policy. The feature remains disabled. +This package runs remote HTTPS MCP operations in a separate Node 24 service. +The official SDK is pinned to 2.4.0. The image pins the maintained Node 24.22.2 +release. The application and each zone keep dispatch disabled by default. ```sh npm ci --ignore-scripts @@ -16,28 +12,135 @@ npm test npm run build ``` -`test/fixtures/server.ts` exposes `startMcpFixture(options)`. Each server binds -only loopback on an operating-system assigned port. It exposes `url`, `origin`, -`fetch`, `ca`, request observations, read/write/cancellation counts, -`setCatalogRevision`, `waitForCall`, `waitForCancellation`, `releaseCalls`, and -`close`. Always close fixtures in test cleanup. - -Options select the protocol version, JSON/SSE replies, a held call barrier, -authentication, a callback issuer fault, and normal/malformed/oversize/binary/ -image/audio/resource/resource-link/error results. Held calls release explicitly; -no sleep or wall-clock delay is needed. A write changes the counter before a -held or hostile reply. Cancellation does not roll that effect back. - -The mock authorization server uses fixed public/confidential test clients and -real TLS. It publishes resource and authorization-server metadata, requires -PKCE S256 and exact resource/redirect/client matches, emits callback `iss`, and -rejects code/refresh replay and scope expansion. Confidential-client credentials -are explicit test strings. It is a test provider, not a credential authority for -the product. - -`test/fixtures/tls.ts` contains a public test CA, certificate, and private key, -valid until 2036. `http.ts` trusts that CA explicitly for fixture requests and -never changes global TLS settings. Never import any fixture module into -production. The loopback listener and private CA do not create production -network exceptions. The mock server is handwritten so tests can send invalid -wire data. Tests use the actual official SDK to check the supported happy paths. +Tests use Node 24, OpenSSL, and Helm. Test files run serially because schema +checks have hard process budgets. Fixtures use local TLS listeners only. The +production entrypoint has no loopback bypass or alternate transport setting. + +## Internal contract + +`src/index.ts` exports the pure DTOs in `contracts.ts`, runtime request validation, +and canonical JSON/hash helpers. These modules do not import the application +store, SDK, network service, or credentials. Hashes use UTF-8 canonical JSON: +object keys sorted with JavaScript string order, array order retained, scalar +encoding from `JSON.stringify`, omitted undefined object properties, and SHA-256 +hex output. Non-JSON values are rejected. Callers in other languages must use +matching canonicalization; token-grant field names are fixed ASCII strings. + +The service accepts only mTLS on port 8443. A caller must present a certificate +signed by the configured caller CA and match an exact SHA-256 certificate pin. +The server certificate also needs the service DNS name and server-auth usage. +The client must check that name and its operator-provided CA. No public ingress, +bearer fallback, generic proxy, or guest listener is provided. + +- `GET /identity` returns protocol version, boot ID, zone ID, policy revision, + and effective policy digest. It requires an approved client certificate. +- `POST /rpc` accepts a versioned `WorkerRequest`. Orchestrator certificates can + discover, execute, validate schemas/destinations, and cancel. Coordinator + certificates can request metadata, exchange tokens, validate destinations, + and cancel. Each operation is fixed; no caller-selected method or header set + is forwarded to a provider. +- Execution scope contains the real session, run, tool-call, and immutable + binding hash. Administration scope contains the trusted request and actor + identities. Execute requires execution scope. Discovery, schema validation, + destination validation, and OAuth require administration scope. Cancel must + match the original scope, caller, connection revision, and credential identity. + +Before acquiring or sending a provider credential, the trusted client must +verify the worker identity against the approved zone and frozen policy digest. +It must authorize current platform state and acquire durable global admission. +Then it creates a new intent bound to that worker boot, policy, connection +revision, credential identity, operation, exact destination, scope, expiry, +and admission lease. The immutable journal `requestDigest` is separate from +`operationDigest`, which hashes the complete transient operation, including its +credential. A routine token refresh can change the latter without changing the +binding or journal identity. Do not persist the transient operation or token. + +The worker consumes each intent before I/O and rejects reuse, expiry, wrong +boot/policy, changed payload, or exhausted local admission. Responses echo the +boot/policy identity, full credential-free intent (including lease expiry and +scope), intent ID, both digests, and lease ID for verification. +A worker restart changes the boot ID. It does not authorize a write retry. +The worker cannot verify database state: only the trusted caller can issue +admission and authorization. It has no database or KEK access. + +Discovery returns upstream DTOs, not a published catalog or a permission grant. +Schema and name fields are retained exactly. Descriptions are untrusted text. +Each input, output, and constraint document is compiled in a separate Ajv +instance inside a disposable process, with its own root and dialect. Independent +constraint documents are never wrapped in a new `allOf` root or URI registry. + +## Runtime bounds + +Every dial validates the exact canonical HTTPS authority and every A/AAAA answer, +then pins one checked address while retaining TLS SNI and HTTP authority. Public +zones reject private and reserved addresses. Internal zones permit only exact +operator hosts within approved private CIDRs. Permanent metadata, loopback, +link-local, multicast, and unspecified ranges cannot be enabled by a zone. +Operator administration denies apply in both modes. OAuth endpoints need their +own approved zone; a private MCP zone does not approve a public OAuth authority. +No redirect, automatic OAuth refresh, or alternate separate-SSE transport runs. + +Each operation owns its SDK client and credential. Legacy sessions never cross +operations, platform sessions, or credential identities. The worker accepts the +three approved protocol versions and JSON/SSE replies. It rejects unsupported +capabilities and content and does not fetch icons, links, resources, or prompts. +It uses low-level SDK requests, blocks synchronous SDK schema compilation, and +permits at most one tool POST per intent, including on protocol or auth errors. + +Limits include 256 KiB arguments, 1 MiB catalog and normalized output, 2 MiB +**decompressed** HTTP bodies, 128 tools, 128 pages, 5 seconds per connection, +60 seconds per tool by default, and a 300-second ceiling. Outer deadlines and +abort signals cover all I/O. Local active caps are 4 per session, 8 per +connection, and 32 executions per worker. Discovery has two slots, OAuth four, +and schema/destination administration four. Schema compilation also has a +shared four-child cap, including children started by execute/discover. These +local caps supplement durable admission across replicas; they do not replace it. + +Schema children have a 128 MiB heap from launch and an empty environment. +Trusted module startup has a separate five-second bound. The parent starts a +hard one-second compile/validation timer **before** sending any schema or data. +No schema enters the child during startup. Deadline, cancellation, process +failure, excess complexity, unsupported dialects, remote references, and unknown +formats fail closed. There is no schema retry or executable custom format. +The Node permission model is not used or claimed as a network sandbox. + +Execution returns an upstream outcome separately from output delivery. A valid +completed response can establish success even when unsupported content, +normalization, size, or output-schema checks prevent delivery. Malformed JSON, +a lost reply, or an HTTP binary reply after a dispatched write leaves the +outcome unknown. Neither a guest error nor cancellation proves that a provider +write failed. Cancellation aborts active I/O; it cannot undo external effects. +Client disconnect and shutdown also abort active operations. The trusted caller +must persist verified results and handle late results and unknown outcomes. + +Known literal credential echoes are redacted from text and structured output. +A discovered name or schema that contains a known secret is rejected, not +rewritten. Error responses contain fixed codes only; raw SDK errors and causes +never leave the boundary. Token-exchange replies go only to coordinator callers. +No provider payload is logged or stored on disk. This does not promise protection +against encoded echoes or a malicious server that already received a credential. + +## Deployment + +`docker/mcp-worker.Dockerfile` builds only this package. The Helm chart has one +Deployment, ClusterIP Service, ConfigMap, ServiceAccount, and NetworkPolicy per +zone. See `deploy/helm/engram/values-mcp-worker.yaml.example`. Provision separate +worker namespaces and RPC TLS secrets before enabling them. Workers get no +service-account token, workload-identity annotation, host mount, DB credential, +KEK, or application environment. They run nonroot with all capabilities dropped, +RuntimeDefault seccomp, a read-only filesystem, and core dumps disabled. Only +the operator config and scoped RPC certificate secret are mounted read-only. + +Public policies use fixed public CIDRs with reserved, metadata, and operator +administration exclusions. Internal policies use operator-supplied CIDRs and +exceptions. Startup validates each rule and refuses missing or invalid exclusions. +DNS is limited to the selected DNS namespace and pods; ingress requires both +the application namespace and trusted component selector. Each zone has its +own identity, service, and policy. Zone policy changes restart workers and change the +policy digest. There is no automatic authorization expansion. + +Kubernetes policies are additive. Operators must verify that no other matching +policy widens these routes, that the CNI enforces them, and that service NAT does +not bypass the intended exclusions. Rendered YAML and local TLS tests do not +prove effective cluster isolation or Firecracker guest isolation. Those release +gates remain required before dispatch can be enabled. diff --git a/mcp-worker/package-lock.json b/mcp-worker/package-lock.json index 440196148..cb6bde17a 100644 --- a/mcp-worker/package-lock.json +++ b/mcp-worker/package-lock.json @@ -10,12 +10,14 @@ "dependencies": { "@modelcontextprotocol/client": "2.4.0", "ajv": "8.20.0", + "ajv-formats": "3.0.1", "ipaddr.js": "2.2.0" }, "devDependencies": { "@types/node": "24.10.0", "prettier": "3.6.2", - "typescript": "5.9.3" + "typescript": "5.9.3", + "yaml": "2.9.1" }, "engines": { "node": ">=24 <25" @@ -77,6 +79,23 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -264,6 +283,22 @@ "node": ">= 8" } }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zod": { "version": "4.6.7", "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", diff --git a/mcp-worker/package.json b/mcp-worker/package.json index 23a13e3bb..e1cc8f3f4 100644 --- a/mcp-worker/package.json +++ b/mcp-worker/package.json @@ -7,7 +7,7 @@ "node": ">=24 <25" }, "scripts": { - "test": "node --test --test-timeout=10000 test/*.test.ts", + "test": "node --test --test-concurrency=1 --test-timeout=30000 test/*.test.ts", "typecheck": "tsc --noEmit", "build": "tsc -p tsconfig.build.json", "format": "prettier --write .", @@ -16,11 +16,13 @@ "dependencies": { "@modelcontextprotocol/client": "2.4.0", "ajv": "8.20.0", + "ajv-formats": "3.0.1", "ipaddr.js": "2.2.0" }, "devDependencies": { "@types/node": "24.10.0", "prettier": "3.6.2", - "typescript": "5.9.3" + "typescript": "5.9.3", + "yaml": "2.9.1" } } diff --git a/mcp-worker/src/contracts.ts b/mcp-worker/src/contracts.ts new file mode 100644 index 000000000..38bb6d322 --- /dev/null +++ b/mcp-worker/src/contracts.ts @@ -0,0 +1,140 @@ +// This wire contract has no application-store or runtime dependencies. +export const WORKER_PROTOCOL = 1; +export const PROTOCOL_VERSIONS = [ + "2026-07-28", + "2025-11-25", + "2025-06-18", +] as const; +export type JsonObject = Record; +export interface ZoneConfig { + id: string; + revision: string; + mode: "public" | "internal"; + authorities: string[]; + aliases: string[]; + cidrs: string[]; + denyCidrs: string[]; + ca?: string | undefined; + limits?: { + execute?: number; + discover?: number; + oauth?: number; + schema?: number; + toolMs?: number; + }; +} +export interface WorkerIdentity { + protocol: 1; + bootId: string; + zoneId: string; + policyDigest: string; + policyRevision: string; +} +export interface ProviderCredential { + header: "authorization" | "x-api-key" | "api-key"; + value: string; +} +export interface UpstreamTool { + name: string; + description?: string; + inputSchema: JsonObject; + outputSchema?: JsonObject; + annotations?: JsonObject; +} +export interface ToolOutput { + content: { type: "text"; text: string }[]; + structuredContent?: unknown; + isError?: boolean; +} +export interface ExecutionResult { + outcome: "succeeded" | "failed" | "outcome_unknown"; + delivery: "available" | "unavailable"; + output?: ToolOutput; + reason?: + | "unsupported_content" + | "result_limit" + | "output_schema" + | "upstream_failure"; +} +export type WorkerOperation = + | { kind: "discover"; endpoint: string; credential?: ProviderCredential } + | { + kind: "execute"; + endpoint: string; + tool: string; + arguments: JsonObject; + inputSchema: JsonObject; + outputSchema?: JsonObject; + argumentConstraintDocuments: JsonObject[]; + classification: "read" | "write"; + credential?: ProviderCredential; + } + | { kind: "validateSchemas"; documents: JsonObject[]; value?: unknown } + | { kind: "validateDestination"; endpoint: string } + | { kind: "metadata"; endpoint: string } + | { + kind: "token"; + endpoint: string; + grant: + | { + grant_type: "authorization_code"; + code: string; + redirect_uri: string; + code_verifier: string; + client_id: string; + resource: string; + client_secret?: string; + } + | { + grant_type: "refresh_token"; + refresh_token: string; + client_id: string; + resource: string; + client_secret?: string; + }; + } + | { kind: "cancel"; targetIntentId: string }; +// A trusted caller creates this only after global durable admission. The worker +// checks its bindings but has no database access to acquire or renew that lease. +export type WorkerScope = + | { + kind: "execution"; + sessionId: string; + runId: string; + toolCallId: string; + bindingHash: string; + } + | { kind: "administration"; requestId: string; actorId: string }; +export interface WorkerIntent { + id: string; + bootId: string; + zoneId: string; + policyDigest: string; + connectionId: string; + connectionRevision: number; + credentialIdentity: string; + scope: WorkerScope; + operation: WorkerOperation["kind"]; + destination: string; + requestDigest: string; + operationDigest: string; + expiresAt: number; + lease: { id: string; deploymentId: string; expiresAt: number }; +} +export interface WorkerRequest { + protocol: 1; + intent: WorkerIntent; + operation: WorkerOperation; +} +export type WorkerResponse = + | { + ok: true; + identity: WorkerIdentity; + intent: WorkerIntent; + intentId: string; + requestDigest: string; + operationDigest: string; + leaseId: string; + result: unknown; + } + | { ok: false; error: string }; diff --git a/mcp-worker/src/deployment.ts b/mcp-worker/src/deployment.ts new file mode 100644 index 000000000..0b9384047 --- /dev/null +++ b/mcp-worker/src/deployment.ts @@ -0,0 +1,69 @@ +import ipaddr from "ipaddr.js"; +import { PERMANENT_DENY_CIDRS } from "./network.ts"; +export interface EgressRule { + cidr: string; + except: string[]; +} +export const RESERVED_CIDRS = [ + "0.0.0.0/8", + "100.64.0.0/10", + "127.0.0.0/8", + "169.254.0.0/16", + "192.0.0.0/24", + "192.0.2.0/24", + "192.88.99.0/24", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "240.0.0.0/4", + "2001::/23", + "2001:db8::/32", + "2002::/16", + ...PERMANENT_DENY_CIDRS, +]; +function contains(outer: string, inner: string) { + const [a, ap] = ipaddr.parseCIDR(outer), + [b, bp] = ipaddr.parseCIDR(inner); + return a.kind() === b.kind() && ap <= bp && b.match(a, ap); +} +export function validateEgressRules( + rules: EgressRule[], + denies: string[], + allowedCidrs?: string[], + mode: "public" | "internal" = "internal", +): void { + if (!rules.length || rules.length > 128) throw Error("egress_rules_required"); + const blocked = [ + ...RESERVED_CIDRS, + ...denies, + ...(mode === "public" + ? ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7"] + : []), + ]; + for (const rule of rules) { + if ( + mode === "internal" && + allowedCidrs && + !allowedCidrs.some((cidr) => contains(cidr, rule.cidr)) + ) + throw Error("egress_exceeds_zone"); + const [address] = ipaddr.parseCIDR(rule.cidr); + if ( + address.kind() === "ipv6" && + !["2000::/3", "fc00::/7"].some((base) => contains(base, rule.cidr)) + ) + throw Error("ipv6_route_denied"); + for (const exception of rule.except) + if (!contains(rule.cidr, exception)) + throw Error("invalid_egress_exception"); + for (const deny of blocked) { + if (contains(deny, rule.cidr)) throw Error("egress_route_denied"); + if ( + contains(rule.cidr, deny) && + !rule.except.some((exception) => contains(exception, deny)) + ) + throw Error("egress_exception_required"); + } + } +} diff --git a/mcp-worker/src/encoding.ts b/mcp-worker/src/encoding.ts new file mode 100644 index 000000000..b89de981c --- /dev/null +++ b/mcp-worker/src/encoding.ts @@ -0,0 +1,29 @@ +import { createHash } from "node:crypto"; +export function object(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +export function canonical(value: unknown): string { + if (value === null || typeof value === "string" || typeof value === "boolean") + return JSON.stringify(value); + if (typeof value === "number" && Number.isFinite(value)) + return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; + if (object(value)) + return `{${Object.keys(value) + .sort() + .filter((k) => value[k] !== undefined) + .map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`) + .join(",")}}`; + throw Error("invalid_json"); +} +export function digest(value: unknown) { + return createHash("sha256").update(canonical(value)).digest("hex"); +} +export function bytes(value: unknown) { + return Buffer.byteLength(JSON.stringify(value)); +} +export function redact(text: string, secrets: string[]): string { + for (const secret of secrets) + if (secret) text = text.split(secret).join("[redacted]"); + return text; +} diff --git a/mcp-worker/src/engine.ts b/mcp-worker/src/engine.ts new file mode 100644 index 000000000..5b6854f1a --- /dev/null +++ b/mcp-worker/src/engine.ts @@ -0,0 +1,339 @@ +import { + Client, + StreamableHTTPClientTransport, +} from "@modelcontextprotocol/client"; +import type { Tool, FetchLike } from "@modelcontextprotocol/client"; +import { PROTOCOL_VERSIONS } from "./contracts.ts"; +import type { + WorkerOperation, + ExecutionResult, + ToolOutput, + UpstreamTool, +} from "./contracts.ts"; +import { bytes, object, redact } from "./encoding.ts"; +import { validateSchemas } from "./schema.ts"; +import { abortable, RESPONSE_BYTES } from "./network.ts"; + +export async function runOperation( + operation: WorkerOperation, + fetch: typeof globalThis.fetch, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + if (operation.kind === "validateSchemas") + return validateSchemas(operation.documents, operation.value, signal); + if (operation.kind === "validateDestination") + throw Error("destination_requires_service"); + if (operation.kind === "cancel") throw Error("cancel_requires_service"); + if (operation.kind === "metadata" || operation.kind === "token") { + const init: RequestInit = { + method: operation.kind === "metadata" ? "GET" : "POST", + redirect: "error", + signal, + headers: { accept: "application/json" }, + }; + if (operation.kind === "token") { + init.body = new URLSearchParams(operation.grant).toString(); + init.headers = { + "content-type": "application/x-www-form-urlencoded", + accept: "application/json", + }; + } + const response = await fetch(operation.endpoint, init); + if (!response.ok) throw Error("upstream_rejected"); + if (!response.headers.get("content-type")?.includes("application/json")) + throw Error("unsupported_response"); + const result: unknown = JSON.parse(await boundedText(response)); + if (!object(result)) throw Error("invalid_response"); + return result; + } + if (operation.kind === "execute") { + if (bytes(operation.arguments) > 256 * 1024) throw Error("arguments_limit"); + if ( + !(await validateSchemas( + [operation.inputSchema, ...operation.argumentConstraintDocuments], + operation.arguments, + signal, + )) + ) + throw Error("arguments_invalid"); + if (operation.outputSchema) + await validateSchemas([operation.outputSchema], undefined, signal); + } + const secrets = + "credential" in operation && operation.credential + ? [ + operation.credential.value, + operation.credential.value.replace(/^Bearer /, ""), + ] + : []; + let verifiedOutcome: ExecutionResult["outcome"] | undefined; + let callSent = false; + let requests = 0; + const scopedFetch: FetchLike = async (input, init) => { + signal.throwIfAborted(); + const target = input instanceof Request ? input.url : String(input); + if (target !== operation.endpoint) throw Error("destination_changed"); + if (++requests > 140) throw Error("request_limit"); + const method = + init?.method ?? (input instanceof Request ? input.method : "GET"); + // Do not open background subscription streams or recover legacy sessions. + if (method === "GET") return new Response(null, { status: 405 }); + if (method !== "POST") throw Error("method_denied"); + if (typeof init?.body !== "string") throw Error("request_body_invalid"); + const message: unknown = JSON.parse(init.body); + if ( + !object(message) || + typeof message.method !== "string" || + ![ + "server/discover", + "initialize", + "notifications/initialized", + "tools/list", + "tools/call", + "notifications/cancelled", + ].includes(message.method) + ) + throw Error("rpc_denied"); + if (message.method === "tools/call") { + if (operation.kind !== "execute" || callSent) + throw Error("repeat_call_denied"); + callSent = true; + } + const headers = new Headers(init.headers); + if (operation.credential) + headers.set(operation.credential.header, operation.credential.value); + // The SDK does not receive the credential through an auth provider or cache. + return fetch(target, { + ...init, + headers, + redirect: "error", + signal: AbortSignal.any([signal, ...(init.signal ? [init.signal] : [])]), + }); + }; + const client = new Client( + { name: "engrams-mcp-worker", version: "1" }, + { + capabilities: {}, + supportedProtocolVersions: [...PROTOCOL_VERSIONS], + versionNegotiation: { + mode: "auto", + probe: { maxRetries: 0, timeoutMs: 5000 }, + }, + inputRequired: { autoFulfill: false }, + // Low-level requests do not compile schemas. Fail closed if an SDK change + // ever invokes its synchronous output validator instead of our child. + jsonSchemaValidator: { + getValidator() { + throw Error("in_process_schema_forbidden"); + }, + }, + }, + ); + client.onerror = () => {}; + const transport = new StreamableHTTPClientTransport( + new URL(operation.endpoint), + { + fetch: scopedFetch, + requestInit: { redirect: "error" }, + reconnectionOptions: { + maxRetries: 0, + initialReconnectionDelay: 0, + maxReconnectionDelay: 0, + reconnectionDelayGrowFactor: 1, + }, + }, + ); + const options = { + signal, + timeout: 300000, + maxTotalTimeout: 300000, + resetTimeoutOnProgress: false, + }; + try { + await abortable( + client.connect(transport, { signal, timeout: 5000 }), + signal, + ); + const version = client.getNegotiatedProtocolVersion(); + if (!PROTOCOL_VERSIONS.some((v) => v === version)) + throw Error("protocol_denied"); + const capabilities = client.getServerCapabilities(); + if ( + !capabilities?.tools || + Object.keys(capabilities).some((k) => !["tools"].includes(k)) + ) + throw Error("capability_denied"); + if (operation.kind === "discover") { + const tools: UpstreamTool[] = []; + let cursor: string | undefined; + const cursors = new Set(); + do { + const page = await client.request( + { method: "tools/list", params: cursor ? { cursor } : {} }, + options, + ); + for (const tool of page.tools) + tools.push(discoveredTool(tool, secrets)); + if (tools.length > 128 || bytes(tools) > 1024 * 1024) + throw Error("catalog_limit"); + cursor = page.nextCursor; + if (cursor) { + if (cursors.has(cursor) || cursors.size >= 128) + throw Error("pagination_limit"); + cursors.add(cursor); + } + } while (cursor); + if (new Set(tools.map((t) => t.name)).size !== tools.length) + throw Error("duplicate_tools"); + await validateSchemas( + tools.flatMap((t) => + t.outputSchema ? [t.inputSchema, t.outputSchema] : [t.inputSchema], + ), + undefined, + signal, + ); + return tools; + } + // Avoid callTool(): it can derive headers, compile schemas, and resend calls. + const result = await client.request( + { + method: "tools/call", + params: { name: operation.tool, arguments: operation.arguments }, + }, + options, + ); + const outcome = result.isError ? "failed" : "succeeded"; + verifiedOutcome = outcome; + if (result.content.some((item) => item.type !== "text")) + return { + outcome, + delivery: "unavailable", + reason: "unsupported_content", + } satisfies ExecutionResult; + const output: ToolOutput = { + content: result.content.flatMap((item) => + item.type === "text" + ? [{ type: "text" as const, text: redact(item.text, secrets) }] + : [], + ), + ...(result.structuredContent !== undefined + ? { structuredContent: redactJson(result.structuredContent, secrets) } + : {}), + ...(result.isError !== undefined ? { isError: result.isError } : {}), + }; + if (bytes(output) > 1024 * 1024) + return { + outcome, + delivery: "unavailable", + reason: "result_limit", + } satisfies ExecutionResult; + if (operation.outputSchema && !result.isError) { + try { + if ( + result.structuredContent === undefined || + !(await validateSchemas( + [operation.outputSchema], + result.structuredContent, + signal, + )) + ) + return { + outcome, + delivery: "unavailable", + reason: "output_schema", + } satisfies ExecutionResult; + } catch { + return { + outcome, + delivery: "unavailable", + reason: "output_schema", + } satisfies ExecutionResult; + } + } + return { outcome, delivery: "available", output } satisfies ExecutionResult; + } catch { + if (operation.kind === "execute") + return { + outcome: verifiedOutcome ?? (callSent ? "outcome_unknown" : "failed"), + delivery: "unavailable", + reason: "upstream_failure", + } satisfies ExecutionResult; + throw Error("discovery_failed"); + } finally { + await client.close().catch(() => {}); + } +} +export function discoveredTool(tool: Tool, secrets: string[]): UpstreamTool { + if ( + containsSecret( + { + name: tool.name, + inputSchema: tool.inputSchema, + outputSchema: tool.outputSchema, + }, + secrets, + ) + ) + throw Error("secret_in_definition"); + if (tool.execution?.taskSupport && tool.execution.taskSupport !== "forbidden") + throw Error("task_tool_denied"); + return { + name: tool.name, + ...(tool.description !== undefined + ? { description: redact(tool.description, secrets) } + : {}), + inputSchema: tool.inputSchema, + ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}), + ...(tool.annotations + ? { annotations: redactJson(tool.annotations, secrets) } + : {}), + }; +} +function redactJson(value: T, secrets: string[]): T; +function redactJson(value: unknown, secrets: string[]): unknown { + if (typeof value === "string") return redact(value, secrets); + if (Array.isArray(value)) + return value.map((item) => redactJson(item, secrets)); + if (object(value)) + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [ + redact(key, secrets), + redactJson(item, secrets), + ]), + ); + return value; +} +async function boundedText(response: Response) { + const reader = response.body?.getReader(); + if (!reader) throw Error("empty_response"); + const chunks: Uint8Array[] = []; + let length = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + length += value.length; + if (length > RESPONSE_BYTES) throw Error("response_limit"); + chunks.push(value); + } + return Buffer.concat(chunks).toString("utf8"); + } finally { + await reader.cancel().catch(() => {}); + } +} + +function containsSecret(value: unknown, secrets: string[]): boolean { + if (typeof value === "string") + return secrets.some( + (secret) => secret.length > 0 && value.includes(secret), + ); + if (Array.isArray(value)) + return value.some((item) => containsSecret(item, secrets)); + if (object(value)) + return Object.entries(value).some( + ([key, item]) => + containsSecret(key, secrets) || containsSecret(item, secrets), + ); + return false; +} diff --git a/mcp-worker/src/index.ts b/mcp-worker/src/index.ts index 839edb2aa..fb942909a 100644 --- a/mcp-worker/src/index.ts +++ b/mcp-worker/src/index.ts @@ -1,2 +1,4 @@ -// Worker policy and service entrypoints are added after the fixture foundation. -export {}; +export type * from "./contracts.ts"; +export { WORKER_PROTOCOL, PROTOCOL_VERSIONS } from "./contracts.ts"; +export { canonical, digest } from "./encoding.ts"; +export { parseWorkerRequest } from "./validation.ts"; diff --git a/mcp-worker/src/main.ts b/mcp-worker/src/main.ts new file mode 100644 index 000000000..52182943b --- /dev/null +++ b/mcp-worker/src/main.ts @@ -0,0 +1,98 @@ +import { readFile } from "node:fs/promises"; +import type { WorkerOptions } from "./service.ts"; +import { createWorker } from "./service.ts"; +import { ZonePolicy } from "./network.ts"; +import { validateEgressRules } from "./deployment.ts"; +import { object } from "./encoding.ts"; +import type { ZoneConfig } from "./contracts.ts"; + +async function main() { + const raw: unknown = JSON.parse( + await readFile("/etc/mcp-worker/config.json", "utf8"), + ); + if ( + !object(raw) || + !isZone(raw.zone) || + !Array.isArray(raw.callers) || + !Array.isArray(raw.egressRules) + ) + throw Error("invalid_config"); + const callers = raw.callers.map((c): WorkerOptions["callers"][number] => { + if ( + !object(c) || + typeof c.fingerprint !== "string" || + !/^([A-F0-9]{2}:){31}[A-F0-9]{2}$/.test(c.fingerprint) || + (c.role !== "orchestrator" && c.role !== "coordinator") + ) + throw Error("invalid_caller"); + return { fingerprint: c.fingerprint, role: c.role }; + }); + const egress = raw.egressRules.map((rule) => { + if (!object(rule) || typeof rule.cidr !== "string" || !strings(rule.except)) + throw Error("invalid_egress"); + return { cidr: rule.cidr, except: rule.except }; + }); + validateEgressRules( + egress, + raw.zone.denyCidrs, + raw.zone.cidrs, + raw.zone.mode, + ); + const tls = { + key: await readFile("/etc/mcp-worker/tls/tls.key", "utf8"), + cert: await readFile("/etc/mcp-worker/tls/tls.crt", "utf8"), + ca: await readFile("/etc/mcp-worker/tls/ca.crt", "utf8"), + }; + const worker = createWorker({ + zone: new ZonePolicy(raw.zone), + tls, + callers, + enabled: process.env.MCP_WORKER_ENABLED === "true", + }); + await worker.listen(); + for (const signal of ["SIGTERM", "SIGINT"] as const) + process.once(signal, () => { + void worker.close().then(() => process.exit(0)); + }); +} +function strings(v: unknown): v is string[] { + return Array.isArray(v) && v.every((s) => typeof s === "string"); +} +function isZone(v: unknown): v is ZoneConfig { + if ( + !object(v) || + typeof v.id !== "string" || + typeof v.revision !== "string" || + !["public", "internal"].includes(String(v.mode)) || + !strings(v.authorities) || + !strings(v.aliases) || + !strings(v.cidrs) || + !strings(v.denyCidrs) || + (v.ca !== undefined && typeof v.ca !== "string") + ) + return false; + if ( + v.limits !== undefined && + (!object(v.limits) || + Object.entries(v.limits).some( + ([key, value]) => + !["execute", "discover", "oauth", "schema", "toolMs"].includes(key) || + !Number.isSafeInteger(value) || + Number(value) < 1, + )) + ) + return false; + return true; +} +process.on("uncaughtException", () => { + process.stderr.write("worker_failed\n"); + process.exit(1); +}); +process.on("unhandledRejection", () => { + process.stderr.write("worker_failed\n"); + process.exit(1); +}); +void main().catch(() => { + process.stderr.write("worker_start_failed\n"); + process.exit(1); +}); diff --git a/mcp-worker/src/network.ts b/mcp-worker/src/network.ts new file mode 100644 index 000000000..721840f5e --- /dev/null +++ b/mcp-worker/src/network.ts @@ -0,0 +1,331 @@ +import { resolve4, resolve6 } from "node:dns/promises"; +import { request as httpsRequest } from "node:https"; +import type { RequestOptions } from "node:https"; +import type { ClientRequest } from "node:http"; +import { createGunzip, createInflate, createBrotliDecompress } from "node:zlib"; +import ipaddr from "ipaddr.js"; +import { digest } from "./encoding.ts"; +import type { ZoneConfig } from "./contracts.ts"; + +export const PERMANENT_DENY_CIDRS = [ + "168.63.129.16/32", + "fd00:ec2::254/128", + "fd20:ce::254/128", + "3fff::/20", +]; +export const RESPONSE_BYTES = 2 * 1024 * 1024; +const allowedHeaders = new Set([ + "accept", + "content-type", + "authorization", + "x-api-key", + "api-key", + "mcp-session-id", + "mcp-protocol-version", + "mcp-method", + "mcp-name", + "last-event-id", +]); +export type NetworkDependencies = { + resolve: (hostname: string) => Promise; + request: (url: URL, options: RequestOptions) => ClientRequest; +}; +const defaults: NetworkDependencies = { + async resolve(hostname) { + const results = await Promise.all( + [resolve4(hostname), resolve6(hostname)].map(async (p) => { + try { + return await p; + } catch (e) { + if ( + e instanceof Error && + "code" in e && + ["ENODATA", "ENOTFOUND"].includes(String(e.code)) + ) + return []; + throw Error("dns_failed"); + } + }), + ); + return results.flat(); + }, + request: (url, options) => httpsRequest(url, options), +}; +export class ZonePolicy { + readonly config: ZoneConfig; + readonly digest: string; + constructor(config: ZoneConfig) { + this.config = structuredClone(config); + if ( + !config.id || + !config.revision || + !["public", "internal"].includes(config.mode) || + !config.authorities.length + ) + throw Error("invalid_zone"); + for (const authority of config.authorities) { + const u = new URL(`https://${authority}/`); + if ( + u.host !== authority || + !validHost(u.hostname) || + u.username || + u.password + ) + throw Error("invalid_authority"); + } + for (const alias of config.aliases) + if (!validHost(alias)) throw Error("invalid_alias"); + for (const cidr of [...config.cidrs, ...config.denyCidrs]) + ipaddr.parseCIDR(cidr); + if (config.mode === "internal") + for (const cidr of config.cidrs) { + const [address, prefix] = ipaddr.parseCIDR(cidr); + if ( + !["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7"].some( + (base) => { + const [network, bits] = ipaddr.parseCIDR(base); + return ( + address.kind() === network.kind() && + prefix >= bits && + address.match(network, bits) + ); + }, + ) + ) + throw Error("internal_cidr_denied"); + } + this.digest = digest({ + version: 1, + policy: config, + permanentDenyCidrs: PERMANENT_DENY_CIDRS, + caIdentity: config.ca ?? "system-roots", + limits: { + execute: 32, + discover: 2, + oauth: 4, + schema: 4, + toolMs: 60000, + ...config.limits, + }, + }); + for (const list of [ + this.config.authorities, + this.config.aliases, + this.config.cidrs, + this.config.denyCidrs, + ]) + Object.freeze(list); + if (this.config.limits) Object.freeze(this.config.limits); + Object.freeze(this.config); + } + endpoint(input: string): URL { + const u = new URL(input); + if ( + input !== u.href || + u.protocol !== "https:" || + u.username || + u.password || + u.search || + u.hash || + !validHost(u.hostname) || + !this.config.authorities.includes(u.host) + ) + throw Error("destination_denied"); + return u; + } + checkAddresses(addresses: string[]) { + if (!addresses.length || addresses.length > 64) throw Error("dns_denied"); + for (const raw of addresses) { + const address = ipaddr.parse(raw); + const range = address.range(); + // IPv4-mapped, translation, transition and reserved IPv6 ranges fail closed. + if (!["unicast", "private", "uniqueLocal"].includes(range)) + throw Error("address_denied"); + if ( + address.kind() === "ipv6" && + range === "unicast" && + !matches(address, "2000::/3") + ) + throw Error("address_denied"); + if ( + [...PERMANENT_DENY_CIDRS, ...this.config.denyCidrs].some((cidr) => + matches(address, cidr), + ) + ) + throw Error("address_denied"); + if (this.config.mode === "public" && range !== "unicast") + throw Error("address_denied"); + if ( + this.config.mode === "internal" && + !this.config.cidrs.some((cidr) => matches(address, cidr)) + ) + throw Error("address_denied"); + } + } +} +function validHost(host: string) { + return ( + !ipaddr.isValid(host.replace(/^\[|\]$/g, "")) && + host.length <= 253 && + host.includes(".") && + host + .split(".") + .every((s) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(s)) + ); +} +function matches(address: ipaddr.IPv4 | ipaddr.IPv6, cidr: string) { + const [network, prefix] = ipaddr.parseCIDR(cidr); + return address.kind() === network.kind() && address.match(network, prefix); +} +// Dependencies are a code-level transport seam, never an operator configuration +// bypass. Production uses only the defaults below. +export function restrictedFetch( + zone: ZonePolicy, + dependencies: NetworkDependencies = defaults, +): typeof fetch { + return async (input, init) => { + const raw = input instanceof Request ? input.url : String(input); + const url = zone.endpoint(raw); + const source = input instanceof Request ? input : undefined; + const signal = init?.signal ?? source?.signal; + signal?.throwIfAborted(); + const headers = new Headers(init?.headers ?? source?.headers); + for (const key of headers.keys()) + if (!allowedHeaders.has(key)) throw Error("header_denied"); + headers.set("accept-encoding", "gzip, deflate, br"); + const method = init?.method ?? source?.method ?? "GET"; + if (!["GET", "POST", "DELETE"].includes(method)) + throw Error("method_denied"); + const body = init?.body ?? (source?.body ? await source.text() : undefined); + if (body !== undefined && body !== null && typeof body !== "string") + throw Error("body_denied"); + if (typeof body === "string" && Buffer.byteLength(body) > 1024 * 1024) + throw Error("request_limit"); + const connectionDeadline = performance.now() + 5000; + const connectionSignal = AbortSignal.any([ + ...(signal ? [signal] : []), + AbortSignal.timeout(5000), + ]); + const addresses = await abortable( + dependencies.resolve(url.hostname), + connectionSignal, + ); + zone.checkAddresses(addresses); + connectionSignal.throwIfAborted(); + const address = addresses[0]!; + return new Promise((resolve, reject) => { + const req = dependencies.request(url, { + method, + headers: { ...Object.fromEntries(headers), host: url.host }, + servername: url.hostname, + family: ipaddr.parse(address).kind() === "ipv4" ? 4 : 6, + rejectUnauthorized: true, + ...(zone.config.ca ? { ca: zone.config.ca } : {}), + agent: false, + // This callback returns the already checked address; it never repeats DNS. + lookup: (_host, _options, callback) => + callback( + null, + address, + ipaddr.parse(address).kind() === "ipv4" ? 4 : 6, + ), + ...(signal ? { signal } : {}), + maxHeaderSize: 16384, + }); + const timer = setTimeout( + () => req.destroy(Error("connect_deadline")), + Math.max(1, connectionDeadline - performance.now()), + ); + req.once("socket", (socket) => + socket.once("secureConnect", () => clearTimeout(timer)), + ); + req.once("error", () => { + clearTimeout(timer); + reject(Error("transport_failed")); + }); + req.once("response", (res) => { + clearTimeout(timer); + const status = res.statusCode ?? 500; + if (status >= 300 && status < 400) { + res.destroy(); + reject(Error("redirect_denied")); + return; + } + const encoding = res.headers["content-encoding"]; + if ( + encoding && + !["identity", "gzip", "deflate", "br"].includes(encoding) + ) { + res.destroy(); + reject(Error("encoding_denied")); + return; + } + const decoder = + encoding === "gzip" + ? createGunzip() + : encoding === "deflate" + ? createInflate() + : encoding === "br" + ? createBrotliDecompress() + : undefined; + const stream = decoder ? res.pipe(decoder) : res; + let received = 0; + const responseHeaders = new Headers(); + for (const [name, value] of Object.entries(res.headers)) + if ( + value !== undefined && + !["content-encoding", "content-length", "set-cookie"].includes(name) + ) + responseHeaders.set( + name, + Array.isArray(value) ? value.join(",") : value, + ); + const bodyStream = new ReadableStream({ + start(controller) { + stream.on("data", (chunk: Buffer) => { + received += chunk.length; + if (received > RESPONSE_BYTES) { + stream.destroy(Error("response_limit")); + res.destroy(); + return; + } + controller.enqueue(chunk); + }); + stream.once("end", () => controller.close()); + stream.once("error", () => + controller.error(Error("response_failed")), + ); + res.once("error", () => { + if (decoder) decoder.destroy(Error("response_failed")); + }); + }, + cancel() { + stream.destroy(); + res.destroy(); + req.destroy(); + }, + }); + resolve( + new Response([204, 205, 304].includes(status) ? null : bodyStream, { + status, + headers: responseHeaders, + }), + ); + }); + req.end(body); + }); + }; +} +export async function abortable( + promise: Promise, + signal: AbortSignal, +): Promise { + signal.throwIfAborted(); + return new Promise((resolve, reject) => { + const abort = () => reject(Error("operation_aborted")); + signal.addEventListener("abort", abort, { once: true }); + promise + .then(resolve, reject) + .finally(() => signal.removeEventListener("abort", abort)); + }); +} diff --git a/mcp-worker/src/schema-child.ts b/mcp-worker/src/schema-child.ts new file mode 100644 index 000000000..0130395c4 --- /dev/null +++ b/mcp-worker/src/schema-child.ts @@ -0,0 +1,100 @@ +import { getHeapStatistics } from "node:v8"; +import { fullFormats } from "ajv-formats/dist/formats.js"; +import { Ajv } from "ajv"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { object } from "./encoding.ts"; +// No network loader, custom keyword, executable format, coercion or mutation. +if (getHeapStatistics().heap_size_limit > 128 * 1024 * 1024) process.exit(1); +process.stdout.write("ready\n"); +let raw = ""; +for await (const part of process.stdin) { + raw += String(part); + if (raw.length > 1024 * 1024) process.exit(1); +} +try { + const request: unknown = JSON.parse(raw); + if (!object(request) || !Array.isArray(request.documents)) throw Error(); + let valid = true; + for (const document of request.documents) { + let nodes = 0; + function guard(node: unknown, depth = 0) { + if (++nodes > 10000 || depth > 64) throw Error(); + if (typeof node === "boolean") return; + if (!object(node)) throw Error(); + if ( + node.$schema !== undefined && + ![ + "http://json-schema.org/draft-07/schema#", + "http://json-schema.org/draft-07/schema", + "https://json-schema.org/draft/2020-12/schema", + ].includes(String(node.$schema)) + ) + throw Error(); + for (const key of ["$ref", "$dynamicRef", "$recursiveRef"]) + if ( + node[key] !== undefined && + (typeof node[key] !== "string" || !node[key].startsWith("#")) + ) + throw Error(); + for (const key of [ + "properties", + "patternProperties", + "$defs", + "definitions", + "dependentSchemas", + ]) + if (object(node[key])) + for (const schema of Object.values(node[key])) + guard(schema, depth + 1); + for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) + if (Array.isArray(node[key])) + for (const schema of node[key]) guard(schema, depth + 1); + for (const key of [ + "items", + "additionalProperties", + "additionalItems", + "contains", + "propertyNames", + "not", + "if", + "then", + "else", + "unevaluatedProperties", + "unevaluatedItems", + ]) + if (node[key] !== undefined) { + if (Array.isArray(node[key])) + for (const schema of node[key]) guard(schema, depth + 1); + else guard(node[key], depth + 1); + } + if (object(node.dependencies)) + for (const schema of Object.values(node.dependencies)) + if (!Array.isArray(schema)) guard(schema, depth + 1); + } + guard(document); + const schema = object(document) ? document : {}; + const Constructor = + schema.$schema === "https://json-schema.org/draft/2020-12/schema" + ? Ajv2020 + : Ajv; + const ajv = new Constructor({ + strictSchema: true, + strictTypes: false, + strictTuples: false, + strictRequired: false, + allErrors: false, + validateFormats: true, + coerceTypes: false, + useDefaults: false, + removeAdditional: false, + inlineRefs: false, + }); + for (const [name, format] of Object.entries(fullFormats)) + ajv.addFormat(name, format); + const check = ajv.compile(document); + if ("value" in request && !check(request.value)) valid = false; + } + process.stdout.write(String(valid)); +} catch { + process.exitCode = 1; +} diff --git a/mcp-worker/src/schema.ts b/mcp-worker/src/schema.ts new file mode 100644 index 000000000..9923f4632 --- /dev/null +++ b/mcp-worker/src/schema.ts @@ -0,0 +1,63 @@ +import childProcess from "node:child_process"; +let activeChildren = 0; +export async function validateSchemas( + documents: unknown[], + value?: unknown, + signal?: AbortSignal, +): Promise { + signal?.throwIfAborted(); + const payload = JSON.stringify({ + documents, + ...(value === undefined ? {} : { value }), + }); + if (documents.length > 384 || Buffer.byteLength(payload) > 1024 * 1024) + throw Error("schema_limit"); + if (activeChildren >= 4) throw Error("schema_capacity"); + const file = new URL( + import.meta.url.endsWith(".ts") ? "./schema-child.ts" : "./schema-child.js", + import.meta.url, + ); + activeChildren++; + return new Promise((resolve, reject) => { + const child = childProcess.spawn( + process.execPath, + ["--max-old-space-size=125", "--max-semi-space-size=1", file.pathname], + { env: {}, stdio: ["pipe", "pipe", "ignore"], cwd: "/" }, + ); + let output = ""; + let failure: string | undefined; + const kill = (reason: string) => { + failure = reason; + child.kill("SIGKILL"); + }; + let ready = false; + let timer = setTimeout(() => kill("schema_bootstrap_deadline"), 5000); + const abort = () => kill("schema_aborted"); + signal?.addEventListener("abort", abort, { once: true }); + child.stdout.on("data", (data: Buffer) => { + output += data.toString(); + if (!ready && output.startsWith("ready\n")) { + ready = true; + output = output.slice(6); + clearTimeout(timer); + timer = setTimeout(() => kill("schema_deadline"), 1000); + // Start the hard schema budget before untrusted bytes enter the child. + child.stdin.end(payload); + } + if (output.length > 1024) kill("schema_output_limit"); + }); + child.stdin.on("error", () => {}); + child.once("error", () => { + failure = "schema_process"; + }); + child.once("close", (code) => { + activeChildren--; + clearTimeout(timer); + signal?.removeEventListener("abort", abort); + if (failure) reject(Error(failure)); + else if (code !== 0 || !["true", "false"].includes(output)) + reject(Error("schema_invalid")); + else resolve(output === "true"); + }); + }); +} diff --git a/mcp-worker/src/service.ts b/mcp-worker/src/service.ts new file mode 100644 index 000000000..90a552655 --- /dev/null +++ b/mcp-worker/src/service.ts @@ -0,0 +1,279 @@ +import { parseWorkerRequest } from "./validation.ts"; +import { createServer } from "node:https"; +import type { ServerOptions } from "node:https"; +import type { IncomingMessage } from "node:http"; +import { TLSSocket } from "node:tls"; +import { randomUUID } from "node:crypto"; +import { resolve4, resolve6 } from "node:dns/promises"; +import { ZonePolicy, restrictedFetch, abortable } from "./network.ts"; +import type { NetworkDependencies } from "./network.ts"; +import { digest, object } from "./encoding.ts"; +import { runOperation } from "./engine.ts"; +import type { + WorkerIdentity, + WorkerRequest, + WorkerOperation, + WorkerResponse, + WorkerIntent, +} from "./contracts.ts"; +export interface WorkerOptions { + zone: ZonePolicy; + tls: Pick; + callers: { fingerprint: string; role: "orchestrator" | "coordinator" }[]; + enabled?: boolean; + network?: NetworkDependencies; +} +export function createWorker(options: WorkerOptions) { + if (Number(process.versions.node.split(".")[0]) !== 24) + throw Error("node_24_required"); + const identity: WorkerIdentity = { + protocol: 1, + bootId: randomUUID(), + zoneId: options.zone.config.id, + policyDigest: options.zone.digest, + policyRevision: options.zone.config.revision, + }; + const used = new Map(); + const leases = new Map(); + const active = new Map< + string, + { + controller: AbortController; + intent: WorkerIntent; + principal: string; + bucket: string; + } + >(); + const fetch = restrictedFetch(options.zone, options.network); + const server = createServer( + { + ...options.tls, + requestCert: true, + rejectUnauthorized: true, + minVersion: "TLSv1.3", + maxHeaderSize: 8192, + }, + (req, res) => { + void handle(req).then( + ({ status, body }) => { + res.writeHead(status, { + "content-type": "application/json", + "cache-control": "no-store", + }); + res.end(JSON.stringify(body)); + }, + () => { + res.writeHead(400, { "content-type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: "invalid_request" })); + }, + ); + }, + ); + server.maxConnections = 128; + server.requestTimeout = 5000; + server.headersTimeout = 5000; + server.timeout = 305000; + server.maxRequestsPerSocket = 1; + async function handle( + req: IncomingMessage, + ): Promise<{ status: number; body: unknown }> { + const deny = (status: number, error: string) => ({ + status, + body: { ok: false, error }, + }); + if (!(req.socket instanceof TLSSocket) || !req.socket.authorized) + return deny(403, "caller_denied"); + const principal = req.socket.getPeerCertificate().fingerprint256; + const caller = options.callers.find((c) => c.fingerprint === principal); + if (!caller) return deny(403, "caller_denied"); + if (req.method === "GET" && req.url === "/identity") + return { status: 200, body: identity }; + if (req.method !== "POST" || req.url !== "/rpc") + return deny(404, "route_denied"); + if (!options.enabled) return deny(503, "worker_disabled"); + const request = parseWorkerRequest(await readBody(req)); + const { intent, operation } = request; + const now = Date.now(); + for (const [id, expiry] of used) if (expiry <= now) used.delete(id); + for (const [id, expiry] of leases) if (expiry <= now) leases.delete(id); + if ( + intent.bootId !== identity.bootId || + intent.zoneId !== identity.zoneId || + intent.policyDigest !== identity.policyDigest || + intent.operation !== operation.kind || + intent.destination !== + ("endpoint" in operation ? operation.endpoint : "") || + intent.operationDigest !== digest(operation) || + intent.expiresAt <= now || + intent.expiresAt > now + 300000 || + intent.lease.expiresAt < intent.expiresAt || + used.has(intent.id) || + leases.has(intent.lease.id) || + used.size >= 4096 || + leases.size >= 4096 || + intent.lease.expiresAt > now + 300000 + ) + return deny(403, "intent_denied"); + const allowed = + caller.role === "coordinator" + ? ["metadata", "token", "validateDestination", "cancel"] + : [ + "discover", + "execute", + "cancel", + "validateSchemas", + "validateDestination", + ]; + if (!allowed.includes(operation.kind)) return deny(403, "operation_denied"); + if ("endpoint" in operation) options.zone.endpoint(operation.endpoint); + // Consume before admission or I/O. Failed admission needs a new intent. + used.set(intent.id, intent.expiresAt); + leases.set(intent.lease.id, intent.lease.expiresAt); + const bucket = + operation.kind === "execute" + ? "execute" + : operation.kind === "discover" + ? "discover" + : operation.kind === "metadata" || operation.kind === "token" + ? "oauth" + : operation.kind === "cancel" + ? "cancel" + : "schema"; + const limits = options.zone.config.limits; + const caps = { + execute: Math.min(limits?.execute ?? 32, 32), + discover: Math.min(limits?.discover ?? 2, 8), + oauth: Math.min(limits?.oauth ?? 4, 8), + schema: Math.min(limits?.schema ?? 4, 8), + cancel: 32, + }; + const running = [...active.values()]; + if ( + running.filter((a) => a.bucket === bucket).length >= caps[bucket] || + (bucket === "execute" && + (running.filter( + (a) => + a.bucket === "execute" && + a.intent.scope.kind === "execution" && + intent.scope.kind === "execution" && + a.intent.scope.sessionId === intent.scope.sessionId, + ).length >= 4 || + running.filter( + (a) => + a.bucket === "execute" && + a.intent.connectionId === intent.connectionId, + ).length >= 8)) + ) + return deny(429, "capacity_exceeded"); + const controller = new AbortController(); + const disconnected = () => controller.abort(); + req.socket.once("close", disconnected); + const maxMs = + bucket === "execute" + ? Math.min(limits?.toolMs ?? 60000, 300000) + : bucket === "cancel" + ? 5000 + : 10000; + const timer = setTimeout( + () => controller.abort(), + Math.min(intent.expiresAt - now, maxMs), + ); + active.set(intent.id, { controller, intent, principal, bucket }); + try { + let result: unknown; + if (operation.kind === "cancel") { + const target = active.get(operation.targetIntentId); + if ( + target && + (target.principal !== principal || + digest(target.intent.scope) !== digest(intent.scope) || + target.intent.connectionId !== intent.connectionId || + target.intent.connectionRevision !== intent.connectionRevision || + target.intent.credentialIdentity !== intent.credentialIdentity) + ) + return deny(403, "cancel_denied"); + target?.controller.abort(); + result = { cancelRequested: !!target }; + } else if (operation.kind === "validateDestination") { + const u = options.zone.endpoint(operation.endpoint); + const addresses = options.network + ? await abortable( + options.network.resolve(u.hostname), + controller.signal, + ) + : ( + await abortable( + Promise.all( + [resolve4(u.hostname), resolve6(u.hostname)].map( + async (p) => { + try { + return await p; + } catch (e) { + if ( + e instanceof Error && + "code" in e && + ["ENODATA", "ENOTFOUND"].includes(String(e.code)) + ) + return []; + throw Error("dns_failed"); + } + }, + ), + ), + controller.signal, + ) + ).flat(); + options.zone.checkAddresses(addresses); + result = { approved: true }; + } else result = await runOperation(operation, fetch, controller.signal); + const body: WorkerResponse = { + ok: true, + identity, + intent, + intentId: intent.id, + requestDigest: intent.requestDigest, + operationDigest: intent.operationDigest, + leaseId: intent.lease.id, + result, + }; + return { status: 200, body }; + } catch { + return deny(422, "operation_failed"); + } finally { + clearTimeout(timer); + req.socket.removeListener("close", disconnected); + controller.abort(); + active.delete(intent.id); + } + } + return { + identity, + async listen(port = 8443, host = "0.0.0.0") { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, host, resolve); + }); + const a = server.address(); + if (!a || typeof a === "string") throw Error("listen_failed"); + return a.port; + }, + async close() { + for (const item of active.values()) item.controller.abort(); + server.closeAllConnections(); + await new Promise((resolve, reject) => + server.close((e) => (e ? reject(e) : resolve())), + ); + }, + }; +} +async function readBody(req: IncomingMessage): Promise { + const chunks: Buffer[] = []; + let length = 0; + for await (const chunk of req) { + const part = Buffer.from(chunk); + length += part.length; + if (length > 1536 * 1024) throw Error("request_limit"); + chunks.push(part); + } + return JSON.parse(Buffer.concat(chunks).toString("utf8")); +} diff --git a/mcp-worker/src/validation.ts b/mcp-worker/src/validation.ts new file mode 100644 index 000000000..0904742d0 --- /dev/null +++ b/mcp-worker/src/validation.ts @@ -0,0 +1,153 @@ +import { object } from "./encoding.ts"; +import type { + WorkerIntent, + WorkerOperation, + WorkerRequest, + WorkerScope, +} from "./contracts.ts"; +function text(value: unknown): value is string { + return typeof value === "string" && value.length > 0 && value.length <= 8192; +} +function schema(value: unknown): value is Record { + return object(value); +} +function schemas(value: unknown): value is Record[] { + return Array.isArray(value) && value.length <= 384 && value.every(schema); +} +function operationValid(value: unknown): value is WorkerOperation { + if (!object(value) || !text(value.kind)) return false; + if ( + "credential" in value && + (!object(value.credential) || + typeof value.credential.header !== "string" || + !["authorization", "api-key", "x-api-key"].includes( + String(value.credential.header), + ) || + !text(value.credential.value) || + /[\r\n]/.test(value.credential.value)) + ) + return false; + if ( + [ + "discover", + "execute", + "metadata", + "token", + "validateDestination", + ].includes(value.kind) && + !text(value.endpoint) + ) + return false; + switch (value.kind) { + case "discover": + case "metadata": + case "validateDestination": + return true; + case "cancel": + return text(value.targetIntentId); + case "validateSchemas": + return schemas(value.documents); + case "execute": + return ( + text(value.tool) && + object(value.arguments) && + schema(value.inputSchema) && + (value.outputSchema === undefined || schema(value.outputSchema)) && + schemas(value.argumentConstraintDocuments) && + (value.classification === "read" || value.classification === "write") + ); + case "token": { + const g = value.grant; + if ( + !object(g) || + !text(g.client_id) || + !text(g.resource) || + (g.client_secret !== undefined && !text(g.client_secret)) + ) + return false; + const base = ["grant_type", "client_id", "resource", "client_secret"]; + if (g.grant_type === "authorization_code") + return ( + text(g.code) && + text(g.redirect_uri) && + text(g.code_verifier) && + Object.keys(g).every((k) => + [...base, "code", "redirect_uri", "code_verifier"].includes(k), + ) + ); + if (g.grant_type === "refresh_token") + return ( + text(g.refresh_token) && + Object.keys(g).every((k) => [...base, "refresh_token"].includes(k)) + ); + return false; + } + default: + return false; + } +} +function intentValid(i: unknown): i is WorkerIntent { + if (!object(i)) return false; + for (const key of [ + "id", + "bootId", + "zoneId", + "policyDigest", + "connectionId", + "credentialIdentity", + "operation", + "requestDigest", + "operationDigest", + ]) + if (!text(i[key])) return false; + return ( + scopeValid(i.scope) && + typeof i.destination === "string" && + Number.isSafeInteger(i.connectionRevision) && + Number(i.connectionRevision) >= 1 && + Number.isSafeInteger(i.expiresAt) && + object(i.lease) && + text(i.lease.id) && + text(i.lease.deploymentId) && + Number.isSafeInteger(i.lease.expiresAt) + ); +} +export function parseWorkerRequest(value: unknown): WorkerRequest { + if ( + !object(value) || + value.protocol !== 1 || + !intentValid(value.intent) || + !operationValid(value.operation) + ) + throw Error("invalid_request"); + if (value.intent.operation !== value.operation.kind) + throw Error("operation_mismatch"); + if ( + value.operation.kind === "execute" && + value.intent.scope.kind !== "execution" + ) + throw Error("execution_scope_required"); + if ( + !["execute", "cancel"].includes(value.operation.kind) && + value.intent.scope.kind !== "administration" + ) + throw Error("administration_scope_required"); + return { protocol: 1, intent: value.intent, operation: value.operation }; +} + +function scopeValid(value: unknown): value is WorkerScope { + if (!object(value)) return false; + if (value.kind === "execution") + return ( + text(value.sessionId) && + text(value.runId) && + text(value.toolCallId) && + typeof value.bindingHash === "string" && + /^[a-f0-9]{64}$/.test(value.bindingHash) + ); + return ( + value.kind === "administration" && + text(value.requestId) && + text(value.actorId) + ); +} diff --git a/mcp-worker/test/contracts.test.ts b/mcp-worker/test/contracts.test.ts new file mode 100644 index 000000000..2e21d097f --- /dev/null +++ b/mcp-worker/test/contracts.test.ts @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { digest, canonical, parseWorkerRequest } from "../src/index.ts"; +import type { WorkerRequest } from "../src/contracts.ts"; +const request: WorkerRequest = { + protocol: 1, + intent: { + id: "intent", + bootId: "boot", + zoneId: "zone", + policyDigest: "a".repeat(64), + connectionId: "connection", + connectionRevision: 1, + credentialIdentity: "credential", + scope: { kind: "administration", requestId: "request", actorId: "actor" }, + operation: "validateSchemas", + destination: "", + requestDigest: "b".repeat(64), + operationDigest: "c".repeat(64), + expiresAt: 100, + lease: { id: "lease", deploymentId: "deployment", expiresAt: 100 }, + }, + operation: { kind: "validateSchemas", documents: [{}] }, +}; +test("wire contract has distinct administration and execution scopes", () => { + assert.deepEqual(parseWorkerRequest(request), request); + assert.throws(() => + parseWorkerRequest({ + ...request, + operation: { + kind: "execute", + endpoint: "https://tools.example/mcp", + tool: "write", + arguments: {}, + inputSchema: {}, + argumentConstraintDocuments: [], + classification: "write", + }, + }), + ); + assert.throws(() => + parseWorkerRequest({ + ...request, + intent: { + ...request.intent, + scope: { + kind: "execution", + sessionId: "session", + runId: "run", + toolCallId: "call", + bindingHash: "d".repeat(64), + }, + }, + }), + ); + assert.throws(() => + parseWorkerRequest({ + ...request, + intent: { ...request.intent, operation: "unknown" }, + }), + ); +}); +test("canonical JSON sorts only object keys and operation digest changes with credential bytes", () => { + assert.equal( + canonical({ z: [2, 1], a: { c: true, b: "text" }, omitted: undefined }), + '{"a":{"b":"text","c":true},"z":[2,1]}', + ); + assert.equal(digest({ a: 1, b: 2 }), digest({ b: 2, a: 1 })); + assert.notEqual( + digest({ credential: { value: "old" } }), + digest({ credential: { value: "new" } }), + ); + assert.throws(() => canonical({ value: Infinity })); +}); diff --git a/mcp-worker/test/deployment.test.ts b/mcp-worker/test/deployment.test.ts new file mode 100644 index 000000000..08e2a1675 --- /dev/null +++ b/mcp-worker/test/deployment.test.ts @@ -0,0 +1,175 @@ +import assert from "node:assert/strict"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { parseAllDocuments } from "yaml"; +import { test } from "node:test"; +import { mkdtempSync, writeFileSync, rmSync, readFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { validateEgressRules, RESERVED_CIDRS } from "../src/deployment.ts"; +test("operator rules cannot open metadata, loopback or platform administration", () => { + assert.throws(() => + validateEgressRules([{ cidr: "0.0.0.0/0", except: [] }], []), + ); + assert.throws(() => + validateEgressRules([{ cidr: "fd00::/8", except: [] }], []), + ); + assert.throws(() => + validateEgressRules([{ cidr: "10.0.0.0/8", except: [] }], ["10.1.0.0/16"]), + ); + validateEgressRules( + [{ cidr: "10.0.0.0/8", except: ["10.1.0.0/16"] }], + ["10.1.0.0/16"], + ); + assert.throws(() => + validateEgressRules([{ cidr: "169.254.169.254/32", except: [] }], []), + ); +}); +test("worker chart defaults off and image pins maintained Node 24", () => { + const chart = new URL("../../deploy/helm/engram", import.meta.url).pathname; + const rendered = execFileSync( + "helm", + ["template", "test", chart, "-f", `${chart}/ci/render-values.yaml`], + { encoding: "utf8", env: { ...process.env, HELM_PLUGINS: "/nonexistent" } }, + ); + assert.equal(rendered.includes("component: mcp-worker"), false); + const dockerfile = readFileSync( + new URL("../../docker/mcp-worker.Dockerfile", import.meta.url), + "utf8", + ); + assert.ok(dockerfile.includes("node:24.22.2-bookworm-slim")); + assert.ok(dockerfile.includes("ulimit -c 0")); +}); + +test("both zone deployments have fixed identity, mounts, namespace and network limits", (t) => { + const chart = new URL("../../deploy/helm/engram", import.meta.url).pathname; + const dir = mkdtempSync(join(tmpdir(), "mcp-render-")); + t.after(() => rmSync(dir, { recursive: true, force: true })); + const zone = { + namespace: "worker-tools", + isolationConfirmed: true, + dispatchEnabled: false, + tlsSecret: "rpc-tls", + callers: [{ fingerprint: "fixture", role: "orchestrator" }], + policy: { + id: "private", + revision: "1", + mode: "internal", + authorities: ["tools.example"], + aliases: [], + cidrs: ["10.20.0.0/16"], + denyCidrs: ["10.20.0.0/24"], + }, + egressRules: [{ cidr: "10.20.0.0/16", except: ["10.20.0.0/24"] }], + ports: [443], + dns: { namespace: "kube-system", podLabels: { "k8s-app": "kube-dns" } }, + }; + const values = { + mcpWorker: { + enabled: true, + zones: [ + zone, + { + ...zone, + namespace: "worker-public", + policy: { + ...zone.policy, + id: "public", + mode: "public", + cidrs: [], + denyCidrs: ["8.8.4.0/24"], + }, + }, + ], + }, + }; + const file = join(dir, "values.json"); + writeFileSync(file, JSON.stringify(values)); + const rendered = execFileSync( + "helm", + [ + "template", + "test", + chart, + "-f", + `${chart}/ci/render-values.yaml`, + "-f", + file, + ], + { encoding: "utf8", env: { ...process.env, HELM_PLUGINS: "/nonexistent" } }, + ); + const docs = parseAllDocuments(rendered) + .map((d) => d.toJS()) + .filter((d) => d?.metadata?.namespace?.startsWith("worker-")); + assert.equal(docs.length, 10); + for (const d of docs.filter((d) => d.kind === "Deployment")) { + const spec = d.spec.template.spec; + assert.equal(spec.automountServiceAccountToken, false); + assert.equal(spec.enableServiceLinks, false); + assert.equal(spec.securityContext.runAsNonRoot, true); + assert.equal(spec.securityContext.seccompProfile.type, "RuntimeDefault"); + assert.equal(spec.containers.length, 1); + const c = spec.containers[0]; + assert.equal(c.securityContext.readOnlyRootFilesystem, true); + assert.equal(c.securityContext.allowPrivilegeEscalation, false); + assert.deepEqual(c.securityContext.capabilities.drop, ["ALL"]); + assert.deepEqual(c.env, [{ name: "MCP_WORKER_ENABLED", value: "false" }]); + assert.equal(spec.volumes.length, 2); + assert.ok(spec.volumes.every((v: { hostPath?: unknown }) => !v.hostPath)); + assert.equal(spec.hostNetwork, undefined); + } + for (const d of docs.filter((d) => d.kind === "NetworkPolicy")) { + assert.equal(d.spec.ingress[0].from.length, 1); + assert.ok(d.spec.ingress[0].from[0].namespaceSelector); + assert.ok(d.spec.ingress[0].from[0].podSelector); + assert.equal(d.spec.egress[0].to.length, 1); + assert.ok(d.spec.egress[0].to[0].namespaceSelector); + assert.ok(d.spec.egress[0].to[0].podSelector); + assert.deepEqual(d.spec.egress[1].ports, [{ protocol: "TCP", port: 443 }]); + } + for (const d of docs.filter((d) => d.kind === "ConfigMap")) { + const config = JSON.parse(d.data["config.json"]); + validateEgressRules( + config.egressRules, + config.zone.denyCidrs, + config.zone.cidrs, + config.zone.mode, + ); + } + assert.ok( + docs + .filter((d) => d.kind === "Service") + .every((d) => d.spec.type === "ClusterIP"), + ); + assert.ok( + docs + .filter((d) => d.kind === "ServiceAccount") + .every( + (d) => + d.automountServiceAccountToken === false && !d.metadata.annotations, + ), + ); +}); + +test("rendered routes cannot exceed private CIDRs or admit private ranges in public zones", () => { + assert.throws(() => + validateEgressRules( + [{ cidr: "10.0.0.0/8", except: [] }], + [], + ["10.20.0.0/16"], + "internal", + ), + ); + assert.throws(() => + validateEgressRules( + [ + { + cidr: "0.0.0.0/0", + except: RESERVED_CIDRS.filter((c) => !c.includes(":")), + }, + ], + [], + [], + "public", + ), + ); +}); diff --git a/mcp-worker/test/engine.test.ts b/mcp-worker/test/engine.test.ts new file mode 100644 index 000000000..ec44ee062 --- /dev/null +++ b/mcp-worker/test/engine.test.ts @@ -0,0 +1,316 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { runOperation, discoveredTool } from "../src/engine.ts"; +import { startMcpFixture } from "./fixtures/server.ts"; +import type { WorkerOperation } from "../src/contracts.ts"; +const execute = ( + endpoint: string, +): Extract => ({ + kind: "execute", + endpoint, + tool: "write_counter", + arguments: { delta: 2 }, + inputSchema: { + type: "object", + properties: { delta: { type: "integer" } }, + required: ["delta"], + }, + argumentConstraintDocuments: [], + classification: "write", +}); +for (const version of ["2026-07-28", "2025-11-25", "2025-06-18"] as const) + for (const reply of ["json", "sse"] as const) + test(`worker uses SDK with bounded discovery and exact results ${version} ${reply}`, async (t) => { + const fixture = await startMcpFixture({ version, reply }); + t.after(() => fixture.close()); + const tools = await runOperation( + { kind: "discover", endpoint: fixture.url }, + fixture.fetch, + AbortSignal.timeout(20000), + ); + assert.equal(Array.isArray(tools) && tools.length, 2); + const result = await runOperation( + execute(fixture.url), + fixture.fetch, + AbortSignal.timeout(20000), + ); + assert.deepEqual(result, { + outcome: "succeeded", + delivery: "available", + output: { + content: [{ type: "text", text: "count=2" }], + structuredContent: { count: 2 }, + isError: false, + }, + }); + assert.equal(fixture.writes, 1); + if (version !== "2026-07-28") + assert.notEqual( + fixture.requests.find((r) => r.method === "tools/list")?.sessionId, + fixture.requests.find((r) => r.method === "tools/call")?.sessionId, + ); + }); +for (const response of [ + "image", + "audio", + "resource", + "resource_link", + "malformed", + "binary", + "error", +] as const) + test(`worker retains upstream outcome independently of ${response} delivery`, async (t) => { + const fixture = await startMcpFixture({ response }); + t.after(() => fixture.close()); + const result = await runOperation( + execute(fixture.url), + fixture.fetch, + AbortSignal.timeout(20000), + ); + assert.equal(fixture.writes, 1); + assert.ok( + typeof result === "object" && result !== null && "outcome" in result, + ); + assert.equal( + result.outcome, + ["malformed", "binary"].includes(response) + ? "outcome_unknown" + : response === "error" + ? "failed" + : "succeeded", + ); + assert.equal( + "delivery" in result && result.delivery, + response === "error" ? "available" : "unavailable", + ); + }); +test("worker rejects input before a tools call and cancels held I/O", async (t) => { + const fixture = await startMcpFixture({ holdCalls: true }); + t.after(() => fixture.close()); + await assert.rejects( + runOperation( + { + ...execute(fixture.url), + argumentConstraintDocuments: [ + { properties: { delta: { maximum: 1 } } }, + ], + }, + fixture.fetch, + AbortSignal.timeout(20000), + ), + ); + assert.equal(fixture.writes, 0); + const controller = new AbortController(); + const pending = runOperation( + execute(fixture.url), + fixture.fetch, + controller.signal, + ); + await fixture.waitForCall(); + controller.abort(); + const result = await pending; + assert.equal( + typeof result === "object" && + result !== null && + "outcome" in result && + result.outcome, + "outcome_unknown", + ); + await fixture.waitForCancellation(); + assert.equal(fixture.writes, 1); +}); + +for (const attack of [ + "header_mismatch", + "input_required", + "version_correction", + "unauthorized", +] as const) + test(`write is sent once after ${attack}`, async (t) => { + const fixture = await startMcpFixture(); + t.after(() => fixture.close()); + let calls = 0; + const fetch: typeof globalThis.fetch = async (input, init) => { + const response = await fixture.fetch(input, init); + const message = + typeof init?.body === "string" ? JSON.parse(init.body) : {}; + if (message.method !== "tools/call") return response; + calls++; + await response.body?.cancel(); + const body = + attack === "input_required" + ? { + jsonrpc: "2.0", + id: message.id, + result: { + resultType: "input_required", + inputRequests: {}, + requestState: "state", + }, + } + : { + jsonrpc: "2.0", + id: message.id, + error: { + code: attack === "header_mismatch" ? -32021 : -32022, + message: "credential-secret", + }, + }; + return new Response(JSON.stringify(body), { + status: attack === "unauthorized" ? 401 : 200, + headers: { "content-type": "application/json" }, + }); + }; + const result = await runOperation( + execute(fixture.url), + fetch, + AbortSignal.timeout(20000), + ); + assert.equal(fixture.writes, 1); + assert.equal(calls, 1); + assert.equal(JSON.stringify(result).includes("credential-secret"), false); + assert.ok( + typeof result === "object" && + result !== null && + "outcome" in result && + result.outcome === "outcome_unknown", + ); + }); +test("discovery redacts descriptions and rejects secret-bearing names and schemas", async (t) => { + const fixture = await startMcpFixture(); + t.after(() => fixture.close()); + let mode = "description"; + const fetch: typeof globalThis.fetch = async (input, init) => { + const response = await fixture.fetch(input, init); + const request = typeof init?.body === "string" ? JSON.parse(init.body) : {}; + if (request.method !== "tools/list") return response; + const body = await response.json(); + if (mode === "description") + body.result.tools[0].description = "echo provider-secret"; + if (mode === "schema") + body.result.tools[0].inputSchema.const = "provider-secret"; + if (mode === "name") body.result.tools[0].name = "provider-secret"; + return new Response(JSON.stringify(body), { + headers: { "content-type": "application/json" }, + }); + }; + const operation: WorkerOperation = { + kind: "discover", + endpoint: fixture.url, + credential: { header: "authorization", value: "Bearer provider-secret" }, + }; + const tools = await runOperation( + operation, + fetch, + AbortSignal.timeout(20000), + ); + assert.ok(Array.isArray(tools)); + assert.equal(tools[0].description, "echo [redacted]"); + for (mode of ["schema", "name"]) + await assert.rejects( + runOperation(operation, fetch, AbortSignal.timeout(20000)), + /discovery_failed/, + ); +}); +test("output schemas and normalized limits do not erase established success", async (t) => { + const fixture = await startMcpFixture(); + t.after(() => fixture.close()); + const result = await runOperation( + { + ...execute(fixture.url), + outputSchema: { type: "object", properties: { count: { const: 999 } } }, + }, + fixture.fetch, + AbortSignal.timeout(20000), + ); + assert.deepEqual(result, { + outcome: "succeeded", + delivery: "unavailable", + reason: "output_schema", + }); + const fetch: typeof globalThis.fetch = async (input, init) => { + const response = await fixture.fetch(input, init); + const request = typeof init?.body === "string" ? JSON.parse(init.body) : {}; + if (request.method !== "tools/call") return response; + const body = await response.json(); + body.result.content = [{ type: "text", text: "x".repeat(1024 * 1024) }]; + return new Response(JSON.stringify(body), { + headers: { "content-type": "application/json" }, + }); + }; + assert.deepEqual( + await runOperation(execute(fixture.url), fetch, AbortSignal.timeout(20000)), + { outcome: "succeeded", delivery: "unavailable", reason: "result_limit" }, + ); + assert.equal(fixture.writes, 2); +}); +test("secret echoes in structured output and text are redacted", async (t) => { + const fixture = await startMcpFixture(); + t.after(() => fixture.close()); + const fetch: typeof globalThis.fetch = async (input, init) => { + const response = await fixture.fetch(input, init); + const request = typeof init?.body === "string" ? JSON.parse(init.body) : {}; + if (request.method !== "tools/call") return response; + const body = await response.json(); + body.result.content = [{ type: "text", text: "echo provider-secret" }]; + body.result.structuredContent = { value: "provider-secret" }; + return new Response(JSON.stringify(body), { + headers: { "content-type": "application/json" }, + }); + }; + const result = await runOperation( + { + ...execute(fixture.url), + credential: { header: "x-api-key", value: "provider-secret" }, + }, + fetch, + AbortSignal.timeout(20000), + ); + assert.equal(JSON.stringify(result).includes("provider-secret"), false); + assert.ok(JSON.stringify(result).includes("[redacted]")); +}); +test("schema secret detection examines strings rather than serialized escapes", () => { + const secret = 'key"with\\escape'; + assert.throws( + () => + discoveredTool( + { + name: "read", + inputSchema: { + type: "object", + properties: { value: { const: secret } }, + }, + }, + [secret], + ), + /secret_in_definition/, + ); +}); +test("unusable deeply nested structured output retains established success", async (t) => { + const fixture = await startMcpFixture(); + t.after(() => fixture.close()); + const fetch: typeof globalThis.fetch = async (input, init) => { + const response = await fixture.fetch(input, init); + const request = typeof init?.body === "string" ? JSON.parse(init.body) : {}; + if (request.method !== "tools/call") return response; + await response.body?.cancel(); + const nested = '{"child":'.repeat(10000) + "0" + "}".repeat(10000); + return new Response( + `{"jsonrpc":"2.0","id":${JSON.stringify(request.id)},"result":{"resultType":"complete","content":[],"isError":false,"structuredContent":${nested}}}`, + { headers: { "content-type": "application/json" } }, + ); + }; + const result = await runOperation( + execute(fixture.url), + fetch, + AbortSignal.timeout(20000), + ); + assert.ok( + typeof result === "object" && + result !== null && + "outcome" in result && + result.outcome === "succeeded", + ); + assert.ok("delivery" in result && result.delivery === "unavailable"); + assert.equal(fixture.writes, 1); +}); diff --git a/mcp-worker/test/fixtures/worker-tls.ts b/mcp-worker/test/fixtures/worker-tls.ts new file mode 100644 index 000000000..c4989fbc2 --- /dev/null +++ b/mcp-worker/test/fixtures/worker-tls.ts @@ -0,0 +1,77 @@ +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execFileSync } from "node:child_process"; +import { X509Certificate } from "node:crypto"; +import { request } from "node:https"; +export async function workerTls() { + const dir = await mkdtemp(join(tmpdir(), "mcp-worker-tls-")); + try { + execFileSync( + "openssl", + [ + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + join(dir, "key.pem"), + "-out", + join(dir, "cert.pem"), + "-days", + "1", + "-subj", + "/CN=tools.example", + "-addext", + "subjectAltName=DNS:tools.example", + "-addext", + "extendedKeyUsage=serverAuth,clientAuth", + ], + { stdio: "ignore" }, + ); + const key = await readFile(join(dir, "key.pem"), "utf8"), + cert = await readFile(join(dir, "cert.pem"), "utf8"); + return { + key, + cert, + ca: cert, + fingerprint: new X509Certificate(cert).fingerprint256, + }; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} +export async function localRpc( + port: number, + tls: Awaited>, + path: string, + body?: unknown, + withCert = true, +) { + return new Promise<{ status: number; body: unknown }>((resolve, reject) => { + const req = request( + { + hostname: "127.0.0.1", + port, + servername: "tools.example", + path, + method: body === undefined ? "GET" : "POST", + ca: tls.ca, + ...(withCert ? { key: tls.key, cert: tls.cert } : {}), + agent: false, + }, + (res) => { + let raw = ""; + res.on("data", (chunk) => { + raw += String(chunk); + }); + res.on("end", () => + resolve({ status: res.statusCode ?? 0, body: JSON.parse(raw) }), + ); + }, + ); + req.on("error", reject); + req.end(body === undefined ? undefined : JSON.stringify(body)); + }); +} diff --git a/mcp-worker/test/network.test.ts b/mcp-worker/test/network.test.ts new file mode 100644 index 000000000..820d7b09f --- /dev/null +++ b/mcp-worker/test/network.test.ts @@ -0,0 +1,196 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { ZonePolicy, restrictedFetch } from "../src/network.ts"; + +const config = { + id: "public", + revision: "1", + mode: "public" as const, + authorities: ["tools.example"], + aliases: [], + cidrs: [], + denyCidrs: ["203.0.113.0/24"], + ca: undefined, +}; +test("destination and all DNS answers are checked before any dial", async () => { + let dials = 0; + for (const answers of [ + ["127.0.0.1"], + ["169.254.169.254"], + ["::1"], + ["::ffff:127.0.0.1"], + ["8.8.8.8", "10.0.0.1"], + ["8.8.8.8", "fd00::1"], + ["203.0.113.1"], + ]) { + const fetch = restrictedFetch(new ZonePolicy(config), { + resolve: async () => answers, + request: () => { + dials++; + throw Error("dial"); + }, + }); + await assert.rejects(fetch("https://tools.example/mcp")); + } + const fetch = restrictedFetch(new ZonePolicy(config), { + resolve: async () => ["8.8.8.8"], + request: () => { + dials++; + throw Error("dial"); + }, + }); + for (const url of [ + "http://tools.example/mcp", + "https://evil.example/mcp", + "https://user@tools.example/mcp", + "https://tools.example/mcp?q=1", + "https://tools.example/mcp#x", + "https://127.0.0.1/mcp", + "https://tools.example:8443/mcp", + "https://tools.example./mcp", + ]) + await assert.rejects(fetch(url)); + assert.equal(dials, 0); +}); +test("internal approval never admits permanent denies and policy digest covers authority", () => { + const zone = new ZonePolicy({ + ...config, + mode: "internal", + cidrs: ["10.0.0.0/8"], + }); + zone.checkAddresses(["10.1.2.3"]); + assert.throws(() => zone.checkAddresses(["127.0.0.1"])); + assert.throws(() => zone.checkAddresses(["169.254.169.254"])); + assert.notEqual( + zone.digest, + new ZonePolicy({ ...config, authorities: ["other.example"] }).digest, + ); +}); + +test("real TLS preserves authority and pins one checked DNS answer on each dial", async (t) => { + const { createServer, request } = await import("node:https"); + const { workerTls } = await import("./fixtures/worker-tls.ts"); + const tls = await workerTls(); + let host = ""; + let sni = ""; + let hits = 0; + const server = createServer(tls, (req, res) => { + hits++; + host = req.headers.host ?? ""; + sni = "servername" in req.socket ? String(req.socket.servername) : ""; + res.end("ok"); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + t.after(() => new Promise((resolve) => server.close(() => resolve()))); + const address = server.address(); + assert.ok(address && typeof address !== "string"); + let resolutions = 0; + let pinned = ""; + const zone = new ZonePolicy({ ...config, ca: tls.ca }); + const fetch = restrictedFetch(zone, { + resolve: async () => { + resolutions++; + return resolutions === 1 ? ["8.8.8.8"] : ["127.0.0.1"]; + }, + request: (url, options) => { + assert.equal(options.servername, "tools.example"); + return request(url, { + ...options, + port: address.port, + lookup: (hostname, lookupOptions, callback) => { + assert.ok(options.lookup); + options.lookup(hostname, lookupOptions, (error, checked, family) => { + assert.equal(error, null); + pinned = String(checked); + callback(null, "127.0.0.1", family); + }); + }, + }); + }, + }); + assert.equal(await (await fetch("https://tools.example/mcp")).text(), "ok"); + assert.equal(pinned, "8.8.8.8"); + assert.equal(host, "tools.example"); + assert.equal(sni, "tools.example"); + await assert.rejects(fetch("https://tools.example/mcp")); + assert.equal(hits, 1); + assert.equal(resolutions, 2); + for (const [ca, authority] of [ + [undefined, "tools.example"], + [tls.ca, "other.example"], + ] as const) { + const bad = restrictedFetch( + new ZonePolicy({ ...config, ca, authorities: [authority] }), + { + resolve: async () => ["8.8.8.8"], + request: (url, options) => + request(url, { + ...options, + hostname: "127.0.0.1", + port: address.port, + }), + }, + ); + await assert.rejects(bad(`https://${authority}/mcp`)); + } + assert.equal(hits, 1); +}); +test("real HTTPS refuses redirects and bounds decompressed bodies", async (t) => { + const { createServer, request } = await import("node:https"); + const { gzipSync } = await import("node:zlib"); + const { workerTls } = await import("./fixtures/worker-tls.ts"); + const tls = await workerTls(); + let hits = 0; + const server = createServer(tls, (req, res) => { + hits++; + if (req.url === "/redirect") { + res.writeHead(302, { location: "https://tools.example/next" }).end(); + return; + } + res + .writeHead(200, { "content-encoding": "gzip" }) + .end(gzipSync("x".repeat(2 * 1024 * 1024 + 1))); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + t.after(() => new Promise((resolve) => server.close(() => resolve()))); + const address = server.address(); + assert.ok(address && typeof address !== "string"); + const fetch = restrictedFetch(new ZonePolicy({ ...config, ca: tls.ca }), { + resolve: async () => ["8.8.8.8"], + request: (url, options) => + request(url, { ...options, hostname: "127.0.0.1", port: address.port }), + }); + await assert.rejects(fetch("https://tools.example/redirect")); + assert.equal(hits, 1); + await assert.rejects((await fetch("https://tools.example/large")).text()); + assert.equal(hits, 2); +}); +test("permanent metadata and non-global IPv6 denies cannot be widened by an internal zone", () => { + const zone = new ZonePolicy({ + ...config, + mode: "internal", + cidrs: ["fc00::/7"], + }); + for (const ip of [ + "168.63.129.16", + "fd00:ec2::254", + "fd20:ce::254", + "fec0::1", + "3fff::1", + "64:ff9b:1::1", + ]) + assert.throws(() => zone.checkAddresses([ip]), ip); +}); +test("internal CIDRs cannot grant public routes and defaults enter policy identity", () => { + assert.throws( + () => + new ZonePolicy({ ...config, mode: "internal", cidrs: ["8.8.8.0/24"] }), + ); + assert.throws( + () => + new ZonePolicy({ ...config, mode: "internal", cidrs: ["10.0.0.0/7"] }), + ); + const a = new ZonePolicy(config); + const b = new ZonePolicy({ ...config, limits: { execute: 31 } }); + assert.notEqual(a.digest, b.digest); +}); diff --git a/mcp-worker/test/schema.test.ts b/mcp-worker/test/schema.test.ts new file mode 100644 index 000000000..a210b6ffc --- /dev/null +++ b/mcp-worker/test/schema.test.ts @@ -0,0 +1,146 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { validateSchemas } from "../src/schema.ts"; +test("independent dialects, roots and local references retain their meaning", async () => { + const a = { + $id: "https://schema.example/a", + $schema: "http://json-schema.org/draft-07/schema#", + definitions: { value: { type: "integer", minimum: 1 } }, + properties: { value: { $ref: "#/definitions/value" } }, + }; + const b = { + $id: "https://schema.example/a", + $schema: "https://json-schema.org/draft/2020-12/schema", + $defs: { value: { type: "integer", maximum: 3 } }, + properties: { value: { $ref: "#/$defs/value" } }, + }; + assert.equal(await validateSchemas([a, b], { value: 2 }), true); + assert.equal(await validateSchemas([a, b], { value: 0 }), false); + assert.equal(await validateSchemas([a, b], { value: 4 }), false); + assert.equal(await validateSchemas([{ type: "number" }], "2"), false); +}); +test("unsupported dialects, remote references and executable formats fail closed", async () => { + for (const schema of [ + { $schema: "https://example.com/schema" }, + { $ref: "https://example.com/schema" }, + { format: "executable" }, + { type: "string", pattern: "[" }, + ]) + await assert.rejects(validateSchemas([schema])); +}); +test("hostile regular expression is killed in disposable process", async () => { + const begin = performance.now(); + await assert.rejects( + validateSchemas( + [{ type: "string", pattern: "^(a+)+$" }], + "a".repeat(100) + "!", + ), + { message: "schema_deadline" }, + ); + assert.ok(performance.now() - begin < 6500); + assert.equal(await validateSchemas([{ type: "integer" }], 1), true); +}); +test("standard formats validate in the child and ordinary data keys remain data", async () => { + assert.equal( + await validateSchemas( + [{ type: "string", format: "date-time" }], + "2026-11-02T12:00:00Z", + ), + true, + ); + assert.equal( + await validateSchemas([{ type: "string", format: "date-time" }], "invalid"), + false, + ); + assert.equal( + await validateSchemas( + [ + { + properties: { + $ref: { const: "https://example.com/a" }, + $schema: { const: "data" }, + }, + }, + ], + { $ref: "https://example.com/a", $schema: "data" }, + ), + true, + ); +}); +test("schema processes have a shared cap and cancellation kills the child", async () => { + const controllers = Array.from({ length: 4 }, () => new AbortController()); + const pending = controllers.map((c) => + validateSchemas( + [{ type: "string", pattern: "^(a+)+$" }], + "a".repeat(100) + "!", + c.signal, + ), + ); + await assert.rejects(validateSchemas([{}]), /capacity/); + for (const controller of controllers) controller.abort(); + for (const result of await Promise.allSettled(pending)) + assert.equal(result.status, "rejected"); +}); +test("trusted bootstrap delay cannot extend the schema timer and can be canceled", async (t) => { + const { default: childProcess } = await import("node:child_process"); + const { pathToFileURL } = await import("node:url"); + const original = childProcess.spawn; + let firstWrite = 0; + const mock = t.mock.method( + childProcess, + "spawn", + ( + command: string, + args: readonly string[], + options: import("node:child_process").SpawnOptionsWithoutStdio, + ) => { + const path = args.at(-1)!; + const code = `await new Promise(r=>setTimeout(r,600)); await import(${JSON.stringify(pathToFileURL(path).href)});`; + const child = original( + command, + [...args.slice(0, -1), "--input-type=module", "--eval", code], + options, + ); + child.stdin?.once("finish", () => { + firstWrite = performance.now(); + }); + return child; + }, + ); + await assert.rejects( + validateSchemas( + [{ type: "string", pattern: "^(a+)+$" }], + "a".repeat(100) + "!", + ), + /schema_deadline/, + ); + assert.ok(firstWrite > 0); + assert.ok(performance.now() - firstWrite < 2500); + const controller = new AbortController(); + const pending = validateSchemas([{}], {}, controller.signal); + controller.abort(); + await assert.rejects(pending, /schema_aborted/); + mock.mock.restore(); +}); +test("the actual disposable V8 heap ceiling is at most 128 MiB", async (t) => { + const { default: childProcess } = await import("node:child_process"); + const { pathToFileURL } = await import("node:url"); + const original = childProcess.spawn; + t.mock.method( + childProcess, + "spawn", + ( + command: string, + args: readonly string[], + options: import("node:child_process").SpawnOptionsWithoutStdio, + ) => { + const code = `import{getHeapStatistics}from'node:v8';if(getHeapStatistics().heap_size_limit>128*1024*1024)process.exit(9);await import(${JSON.stringify(pathToFileURL(args.at(-1)!).href)});`; + return original( + command, + [...args.slice(0, -1), "--input-type=module", "--eval", code], + options, + ); + }, + ); + assert.equal(await validateSchemas([{ type: "integer" }], 1), true); +}); diff --git a/mcp-worker/test/service.test.ts b/mcp-worker/test/service.test.ts new file mode 100644 index 000000000..b894993b1 --- /dev/null +++ b/mcp-worker/test/service.test.ts @@ -0,0 +1,256 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { randomUUID } from "node:crypto"; +import { createWorker } from "../src/service.ts"; +import { digest } from "../src/encoding.ts"; +import { ZonePolicy } from "../src/network.ts"; +import { workerTls, localRpc } from "./fixtures/worker-tls.ts"; +import type { + WorkerIdentity, + WorkerOperation, + WorkerRequest, +} from "../src/contracts.ts"; +const zone = () => + new ZonePolicy({ + id: "public", + revision: "1", + mode: "public", + authorities: ["tools.example"], + aliases: [], + cidrs: [], + denyCidrs: [], + }); +function envelope( + identity: WorkerIdentity, + operation: WorkerOperation, +): WorkerRequest { + return { + protocol: 1, + intent: { + id: randomUUID(), + ...identity, + connectionId: "connection", + connectionRevision: 1, + credentialIdentity: "credential", + scope: { + kind: "administration", + requestId: "admin-request", + actorId: "actor", + }, + operation: operation.kind, + destination: "endpoint" in operation ? operation.endpoint : "", + requestDigest: "a".repeat(64), + operationDigest: digest(operation), + expiresAt: Date.now() + 10000, + lease: { + id: randomUUID(), + deploymentId: "deployment", + expiresAt: Date.now() + 10000, + }, + }, + operation, + }; +} +test("mTLS RPC binds boot, policy, payload and lease; each intent is single use", async (t) => { + const tls = await workerTls(); + const worker = createWorker({ + zone: zone(), + tls, + callers: [{ fingerprint: tls.fingerprint, role: "orchestrator" }], + enabled: true, + }); + const port = await worker.listen(0, "127.0.0.1"); + t.after(() => worker.close()); + await assert.rejects(localRpc(port, tls, "/identity", undefined, false)); + const hello = await localRpc(port, tls, "/identity"); + assert.equal(hello.status, 200); + assert.deepEqual(hello.body, worker.identity); + const op: WorkerOperation = { + kind: "validateSchemas", + documents: [{ type: "integer" }], + value: 1, + }; + for (const field of ["bootId", "policyDigest", "operationDigest"] as const) { + const request = envelope(worker.identity, op); + request.intent[field] = "wrong"; + assert.equal((await localRpc(port, tls, "/rpc", request)).status, 403); + } + const expired = envelope(worker.identity, op); + expired.intent.lease.expiresAt = 0; + assert.equal((await localRpc(port, tls, "/rpc", expired)).status, 403); + const request = envelope(worker.identity, op); + const result = await localRpc(port, tls, "/rpc", request); + assert.equal(result.status, 200); + assert.ok( + typeof result.body === "object" && + result.body !== null && + "intent" in result.body, + ); + assert.deepEqual(result.body.intent, request.intent); + assert.ok( + typeof result.body === "object" && + result.body !== null && + "result" in result.body && + result.body.result === true, + ); + assert.equal((await localRpc(port, tls, "/rpc", request)).status, 403); + assert.equal((await localRpc(port, tls, "/proxy", request)).status, 404); + assert.equal( + ( + await localRpc( + port, + tls, + "/rpc", + envelope(worker.identity, { + kind: "token", + endpoint: "https://tools.example/token", + grant: { + grant_type: "refresh_token", + refresh_token: "secret", + client_id: "client", + resource: "https://tools.example/mcp", + }, + }), + ) + ).status, + 403, + ); +}); +test("disabled worker and unknown client identity cannot dispatch", async (t) => { + const tls = await workerTls(); + const disabled = createWorker({ + zone: zone(), + tls, + callers: [{ fingerprint: tls.fingerprint, role: "orchestrator" }], + }); + const port = await disabled.listen(0, "127.0.0.1"); + t.after(() => disabled.close()); + assert.equal( + ( + await localRpc( + port, + tls, + "/rpc", + envelope(disabled.identity, { kind: "validateSchemas", documents: [] }), + ) + ).status, + 503, + ); + const unknown = createWorker({ + zone: zone(), + tls, + callers: [{ fingerprint: "00", role: "orchestrator" }], + enabled: true, + }); + const other = await unknown.listen(0, "127.0.0.1"); + t.after(() => unknown.close()); + assert.equal((await localRpc(other, tls, "/identity")).status, 403); +}); +test("OAuth admission is separate, destination validation does not dial, and scoped cancel aborts I/O", async (t) => { + const { createServer, request } = await import("node:https"); + const tls = await workerTls(); + let hits = 0; + let arrived: () => void = () => {}; + const arrival = new Promise((resolve) => { + arrived = resolve; + }); + const provider = createServer(tls, () => { + hits++; + arrived(); + }); + await new Promise((resolve) => + provider.listen(0, "127.0.0.1", resolve), + ); + t.after(() => { + provider.closeAllConnections(); + return new Promise((resolve) => provider.close(() => resolve())); + }); + const address = provider.address(); + assert.ok(address && typeof address !== "string"); + const policy = new ZonePolicy({ + id: "public", + revision: "1", + mode: "public", + authorities: ["tools.example"], + aliases: [], + cidrs: [], + denyCidrs: [], + ca: tls.ca, + limits: { oauth: 1 }, + }); + const worker = createWorker({ + zone: policy, + tls, + callers: [{ fingerprint: tls.fingerprint, role: "coordinator" }], + enabled: true, + network: { + resolve: async () => ["8.8.8.8"], + request: (url, options) => + request(url, { ...options, hostname: "127.0.0.1", port: address.port }), + }, + }); + const port = await worker.listen(0, "127.0.0.1"); + t.after(() => worker.close()); + const first = envelope(worker.identity, { + kind: "metadata", + endpoint: "https://tools.example/metadata", + }); + const pending = localRpc(port, tls, "/rpc", first); + void pending.catch(() => {}); + await arrival; + assert.equal( + ( + await localRpc( + port, + tls, + "/rpc", + envelope(worker.identity, { + kind: "metadata", + endpoint: "https://tools.example/metadata", + }), + ) + ).status, + 429, + ); + assert.equal( + ( + await localRpc( + port, + tls, + "/rpc", + envelope(worker.identity, { + kind: "validateDestination", + endpoint: "https://tools.example/metadata", + }), + ) + ).status, + 200, + ); + assert.equal(hits, 1); + const wrong = envelope(worker.identity, { + kind: "cancel", + targetIntentId: first.intent.id, + }); + wrong.intent.scope = { + kind: "administration", + requestId: "different", + actorId: "actor", + }; + assert.equal((await localRpc(port, tls, "/rpc", wrong)).status, 403); + assert.equal( + ( + await localRpc( + port, + tls, + "/rpc", + envelope(worker.identity, { + kind: "cancel", + targetIntentId: first.intent.id, + }), + ) + ).status, + 200, + ); + assert.equal((await pending).status, 422); + assert.equal(hits, 1); +}); From 34327914b1b0783ba098bf2c657f58cfa87eb8c7 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 22:35:37 -0500 Subject: [PATCH 007/279] Preserve schema UTF-8 and contain upstream response errors --- mcp-worker/src/network.ts | 144 ++++++++++++++++++-------------- mcp-worker/src/schema-child.ts | 1 + mcp-worker/test/network.test.ts | 52 ++++++++++++ mcp-worker/test/schema.test.ts | 50 +++++++++++ 4 files changed, 184 insertions(+), 63 deletions(-) diff --git a/mcp-worker/src/network.ts b/mcp-worker/src/network.ts index 721840f5e..42a5f95c1 100644 --- a/mcp-worker/src/network.ts +++ b/mcp-worker/src/network.ts @@ -2,6 +2,7 @@ import { resolve4, resolve6 } from "node:dns/promises"; import { request as httpsRequest } from "node:https"; import type { RequestOptions } from "node:https"; import type { ClientRequest } from "node:http"; +import type { Transform } from "node:stream"; import { createGunzip, createInflate, createBrotliDecompress } from "node:zlib"; import ipaddr from "ipaddr.js"; import { digest } from "./encoding.ts"; @@ -245,72 +246,89 @@ export function restrictedFetch( }); req.once("response", (res) => { clearTimeout(timer); - const status = res.statusCode ?? 500; - if (status >= 300 && status < 400) { - res.destroy(); - reject(Error("redirect_denied")); - return; - } - const encoding = res.headers["content-encoding"]; - if ( - encoding && - !["identity", "gzip", "deflate", "br"].includes(encoding) - ) { - res.destroy(); - reject(Error("encoding_denied")); - return; - } - const decoder = - encoding === "gzip" - ? createGunzip() - : encoding === "deflate" - ? createInflate() - : encoding === "br" - ? createBrotliDecompress() - : undefined; - const stream = decoder ? res.pipe(decoder) : res; - let received = 0; - const responseHeaders = new Headers(); - for (const [name, value] of Object.entries(res.headers)) + let decoder: Transform | undefined; + try { + const status = res.statusCode; if ( - value !== undefined && - !["content-encoding", "content-length", "set-cookie"].includes(name) + status === undefined || + !Number.isInteger(status) || + status < 200 || + status > 599 ) - responseHeaders.set( - name, - Array.isArray(value) ? value.join(",") : value, - ); - const bodyStream = new ReadableStream({ - start(controller) { - stream.on("data", (chunk: Buffer) => { - received += chunk.length; - if (received > RESPONSE_BYTES) { - stream.destroy(Error("response_limit")); - res.destroy(); - return; - } - controller.enqueue(chunk); - }); - stream.once("end", () => controller.close()); - stream.once("error", () => - controller.error(Error("response_failed")), - ); - res.once("error", () => { - if (decoder) decoder.destroy(Error("response_failed")); - }); - }, - cancel() { - stream.destroy(); + throw Error("response_invalid"); + if (status >= 300 && status < 400) { + res.destroy(); + reject(Error("redirect_denied")); + return; + } + const encoding = res.headers["content-encoding"]; + if ( + encoding && + !["identity", "gzip", "deflate", "br"].includes(encoding) + ) { res.destroy(); - req.destroy(); - }, - }); - resolve( - new Response([204, 205, 304].includes(status) ? null : bodyStream, { - status, - headers: responseHeaders, - }), - ); + reject(Error("encoding_denied")); + return; + } + decoder = + encoding === "gzip" + ? createGunzip() + : encoding === "deflate" + ? createInflate() + : encoding === "br" + ? createBrotliDecompress() + : undefined; + const stream = decoder ? res.pipe(decoder) : res; + let received = 0; + const responseHeaders = new Headers(); + for (const [name, value] of Object.entries(res.headers)) + if ( + value !== undefined && + !["content-encoding", "content-length", "set-cookie"].includes( + name, + ) + ) + responseHeaders.set( + name, + Array.isArray(value) ? value.join(",") : value, + ); + const bodyStream = new ReadableStream({ + start(controller) { + stream.on("data", (chunk: Buffer) => { + received += chunk.length; + if (received > RESPONSE_BYTES) { + stream.destroy(Error("response_limit")); + res.destroy(); + return; + } + controller.enqueue(chunk); + }); + stream.once("end", () => controller.close()); + stream.once("error", () => + controller.error(Error("response_failed")), + ); + res.once("error", () => { + if (decoder) decoder.destroy(Error("response_failed")); + }); + }, + cancel() { + stream.destroy(); + res.destroy(); + req.destroy(); + }, + }); + resolve( + new Response([204, 205, 304].includes(status) ? null : bodyStream, { + status, + headers: responseHeaders, + }), + ); + } catch { + decoder?.destroy(); + res.destroy(); + req.destroy(); + reject(Error("response_invalid")); + } }); req.end(body); }); diff --git a/mcp-worker/src/schema-child.ts b/mcp-worker/src/schema-child.ts index 0130395c4..010056cff 100644 --- a/mcp-worker/src/schema-child.ts +++ b/mcp-worker/src/schema-child.ts @@ -7,6 +7,7 @@ import { object } from "./encoding.ts"; if (getHeapStatistics().heap_size_limit > 128 * 1024 * 1024) process.exit(1); process.stdout.write("ready\n"); let raw = ""; +process.stdin.setEncoding("utf8"); for await (const part of process.stdin) { raw += String(part); if (raw.length > 1024 * 1024) process.exit(1); diff --git a/mcp-worker/test/network.test.ts b/mcp-worker/test/network.test.ts index 820d7b09f..95c3523fa 100644 --- a/mcp-worker/test/network.test.ts +++ b/mcp-worker/test/network.test.ts @@ -194,3 +194,55 @@ test("internal CIDRs cannot grant public routes and defaults enter policy identi const b = new ZonePolicy({ ...config, limits: { execute: 31 } }); assert.notEqual(a.digest, b.digest); }); + +for (const fault of ["status", "header"] as const) { + test( + `invalid upstream ${fault} rejects only its operation`, + { timeout: 3000 }, + async (t) => { + const { createServer, request } = await import("node:https"); + const { workerTls } = await import("./fixtures/worker-tls.ts"); + const tls = await workerTls(); + let requests = 0; + const server = createServer(tls, (_req, res) => { + requests++; + res + .writeHead(requests === 1 && fault === "status" ? 600 : 200) + .end("ok"); + }); + await new Promise((resolve) => + server.listen(0, "127.0.0.1", resolve), + ); + t.after(() => { + server.closeAllConnections(); + server.close(); + }); + const address = server.address(); + assert.ok(address && typeof address !== "string"); + let conversions = 0; + const fetch = restrictedFetch(new ZonePolicy({ ...config, ca: tls.ca }), { + resolve: async () => ["8.8.8.8"], + request: (url, options) => { + const req = request(url, { + ...options, + hostname: "127.0.0.1", + port: address.port, + }); + req.prependOnceListener("response", (res) => { + if (++conversions === 1 && fault === "header") + res.headers["invalid\nname"] = "untrusted"; + }); + return req; + }, + }); + await assert.rejects(fetch("https://tools.example/mcp"), { + message: "response_invalid", + }); + assert.equal( + await (await fetch("https://tools.example/mcp")).text(), + "ok", + ); + assert.equal(requests, 2); + }, + ); +} diff --git a/mcp-worker/test/schema.test.ts b/mcp-worker/test/schema.test.ts index a210b6ffc..1addbaadc 100644 --- a/mcp-worker/test/schema.test.ts +++ b/mcp-worker/test/schema.test.ts @@ -144,3 +144,53 @@ test("the actual disposable V8 heap ceiling is at most 128 MiB", async (t) => { ); assert.equal(await validateSchemas([{ type: "integer" }], 1), true); }); + +test("split UTF-8 preserves schema constants and argument values", async () => { + const { spawn } = await import("node:child_process"); + const results: string[] = []; + for (const [value, splitValue] of [ + ["é", false], + ["��", false], + ["é", true], + ] as const) { + const child = spawn( + process.execPath, + [ + "--max-old-space-size=125", + "--max-semi-space-size=1", + new URL("../src/schema-child.ts", import.meta.url).pathname, + ], + { + env: {}, + stdio: ["pipe", "pipe", "ignore"], + signal: AbortSignal.timeout(5000), + }, + ); + const result = new Promise((resolve, reject) => { + let output = ""; + let sent = false; + child.on("error", reject); + child.stdout.on("data", (chunk: Buffer) => { + output += chunk.toString(); + if (!sent && output.startsWith("ready\n")) { + sent = true; + const payload = Buffer.from( + JSON.stringify({ documents: [{ const: "é" }], value }), + ); + const split = + (splitValue + ? payload.lastIndexOf(Buffer.from("é")) + : payload.indexOf(Buffer.from("é"))) + 1; + child.stdin.write(payload.subarray(0, split)); + setTimeout(() => child.stdin.end(payload.subarray(split)), 50); + } + }); + child.on("close", (code) => { + if (code !== 0) reject(Error("child_failed")); + else resolve(output); + }); + }); + results.push(await result); + } + assert.deepEqual(results, ["ready\ntrue", "ready\nfalse", "ready\ntrue"]); +}); From 8043239c561f592f0bac05b01774f6a43be88db9 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 22:56:41 -0500 Subject: [PATCH 008/279] Reserve MCP credentials and block generic guest access --- .../src/grpc_app/convert.rs | 22 +- crates/engram-coordinator/src/integrations.rs | 40 ++++ crates/engram-coordinator/src/oauth.rs | 29 ++- crates/engram-coordinator/src/org_secrets.rs | 29 ++- .../tests/mcp_credentials.rs | 131 +++++++++++ crates/engram-core/src/traits/metadata.rs | 30 +++ crates/engram-core/src/traits/secrets.rs | 15 ++ crates/engram-core/src/types/org_secret.rs | 25 +++ crates/engram-postgres/src/lib.rs | 127 ++++++++++- crates/engram-sim/src/meta/mod.rs | 11 + crates/engram-sim/src/meta/store_impl.rs | 147 ++++++++++++- crates/engram-sim/tests/meta_conformance.rs | 208 ++++++++++++++++++ .../0126_mcp_secret_reservations.sql | 10 + 13 files changed, 799 insertions(+), 25 deletions(-) create mode 100644 crates/engram-coordinator/tests/mcp_credentials.rs create mode 100644 deploy/migrations/0126_mcp_secret_reservations.sql diff --git a/crates/engram-coordinator/src/grpc_app/convert.rs b/crates/engram-coordinator/src/grpc_app/convert.rs index 1b6678d04..d2978d72b 100644 --- a/crates/engram-coordinator/src/grpc_app/convert.rs +++ b/crates/engram-coordinator/src/grpc_app/convert.rs @@ -186,7 +186,11 @@ pub(crate) fn create_request_from_proto( Ok(app::OauthSubjectKind::Connector) => { engram_core::types::oauth::OAuthSubjectKind::Connector } - Ok(app::OauthSubjectKind::Mcp) => engram_core::types::oauth::OAuthSubjectKind::Mcp, + Ok(app::OauthSubjectKind::Mcp) => { + return Err(ApiError::BadRequest( + "MCP credentials cannot be bound to a guest session".into(), + )) + } Ok(app::OauthSubjectKind::UserConnector) => { engram_core::types::oauth::OAuthSubjectKind::UserConnector } @@ -970,6 +974,22 @@ pub(super) fn retirement_to_proto( // tests drive a live system; wall clock/OS entropy here is input, not a decision source (ADR 0098 D1) #[allow(clippy::disallowed_methods)] mod tests { + #[test] + fn mcp_guest_create_is_rejected() { + let request = engram_protocol::app::CreateSessionRequest { + image_uri: "registry.test/example:latest".into(), + oauth_credential: Some(engram_protocol::app::OAuthCredentialBinding { + subject: Some(engram_protocol::app::OAuthSubject { + kind: engram_protocol::app::OauthSubjectKind::Mcp as i32, + id: "connection-a".into(), + }), + provider: "mcp".into(), + }), + ..Default::default() + }; + assert!(super::create_request_from_proto(request).is_err()); + } + use super::*; use engram_core::types::session::SessionMode; use engram_core::types::{Session, SessionState}; diff --git a/crates/engram-coordinator/src/integrations.rs b/crates/engram-coordinator/src/integrations.rs index 02d787277..c405c2066 100644 --- a/crates/engram-coordinator/src/integrations.rs +++ b/crates/engram-coordinator/src/integrations.rs @@ -112,6 +112,18 @@ impl IntegrationBroker { return Some(e.clone()); } let desc = self.by_provider.get(provider)?; + // Cache notifications are not an authorization fence. Check each ref + // before returning an engine that already holds a plaintext value. + for field in &desc.fields { + let name = format!("{}.{}", desc.kind, field.name); + if secrets + .check_access(&name, &SecretSchema::default()) + .await + .is_err() + { + return None; + } + } let version = self.version.load(Ordering::Acquire); if let Some((v, eng)) = self.lazy.lock().get(provider) { if *v == version { @@ -170,6 +182,34 @@ impl IntegrationBroker { mod tests { // tests drive a live system; wall clock/OS entropy here is input, not a decision source (ADR 0098 D1) #![allow(clippy::disallowed_methods)] + #[tokio::test] + async fn mcp_reservation_blocks_cached_mint_before_notification() { + use engram_core::traits::{LayeredSecretStore, MetadataStore, StaticSecretStore}; + use engram_core::types::org_secret::McpSecretRole; + let meta = engram_sim::SimMetadataStore::new( + engram_sim::ManualClock::new(), + Arc::new(engram_sim::SimEntropy::seeded(4)), + ); + let secrets: Arc = Arc::new(LayeredSecretStore::new(vec![ + Arc::new(crate::org_secrets::OrgSecretBackend::new( + meta.clone(), + Arc::new(engram_crypto::EnvVarKeyProvider::from_bytes( + [7; 32], "test", + )), + )), + Arc::new(StaticSecretStore::new(HashMap::from([( + "fake_kind.token".into(), + "cached-value".into(), + )]))), + ])); + let broker = broker(); + assert!(broker.resolve("fake", &secrets).await.is_some()); + meta.reserve_mcp_org_secret("fake_kind.token", "connection-a", McpSecretRole::ApiKey) + .await + .unwrap(); + assert!(broker.resolve("fake", &secrets).await.is_none()); + } + use super::*; use async_trait::async_trait; use engram_core::error::{IntegrationError, SecretError}; diff --git a/crates/engram-coordinator/src/oauth.rs b/crates/engram-coordinator/src/oauth.rs index 33c27f094..ebb0453f5 100644 --- a/crates/engram-coordinator/src/oauth.rs +++ b/crates/engram-coordinator/src/oauth.rs @@ -311,15 +311,28 @@ impl OAuthManager { .await?) } - pub async fn fetch_session( + async fn guest_binding( &self, session_id: SessionId, - ) -> Result<(String, i64, Vec), OAuthServiceError> { + ) -> Result { let binding = self .meta .get_session_oauth_binding(session_id) .await? .ok_or(OAuthServiceError::NotFound)?; + if binding.key.subject_kind == OAuthSubjectKind::Mcp { + return Err(OAuthServiceError::BadRequest( + "MCP credentials are not available to guest sessions".into(), + )); + } + Ok(binding) + } + + pub async fn fetch_session( + &self, + session_id: SessionId, + ) -> Result<(String, i64, Vec), OAuthServiceError> { + let binding = self.guest_binding(session_id).await?; let row = self .meta .get_oauth_credential(&binding.key) @@ -339,11 +352,7 @@ impl OAuthManager { expected_version: i64, payload: &[u8], ) -> Result<(String, i64, Vec), OAuthServiceError> { - let binding = self - .meta - .get_session_oauth_binding(session_id) - .await? - .ok_or(OAuthServiceError::NotFound)?; + let binding = self.guest_binding(session_id).await?; let current = self .meta .get_oauth_credential(&binding.key) @@ -384,11 +393,7 @@ impl OAuthManager { expected_version: i64, reason: &str, ) -> Result<(), OAuthServiceError> { - let binding = self - .meta - .get_session_oauth_binding(session_id) - .await? - .ok_or(OAuthServiceError::NotFound)?; + let binding = self.guest_binding(session_id).await?; match self .meta .mark_oauth_credential_broken(&binding.key, expected_version, reason) diff --git a/crates/engram-coordinator/src/org_secrets.rs b/crates/engram-coordinator/src/org_secrets.rs index 46adb4b35..8fefebe49 100644 --- a/crates/engram-coordinator/src/org_secrets.rs +++ b/crates/engram-coordinator/src/org_secrets.rs @@ -39,12 +39,39 @@ impl OrgSecretBackend { #[async_trait] impl SecretStore for OrgSecretBackend { + async fn check_access(&self, name: &str, schema: &SecretSchema) -> Result<(), SecretError> { + for reference in [ + Some(name), + schema + .r#ref + .as_deref() + .filter(|reference| *reference != name), + ] + .into_iter() + .flatten() + { + if self + .meta + .get_mcp_org_secret_reservation(reference) + .await + .map_err(|e| SecretError::Backend(Box::new(e)))? + .is_some() + { + return Err(SecretError::Unauthorized( + "credential is reserved for MCP".into(), + )); + } + } + Ok(()) + } + async fn get( &self, _ctx: &SecretContext<'_>, name: &str, - _schema: &SecretSchema, + schema: &SecretSchema, ) -> Result, SecretError> { + self.check_access(name, schema).await?; let Some(sealed) = self .meta .get_org_secret_sealed(name) diff --git a/crates/engram-coordinator/tests/mcp_credentials.rs b/crates/engram-coordinator/tests/mcp_credentials.rs new file mode 100644 index 000000000..8fab6ed5c --- /dev/null +++ b/crates/engram-coordinator/tests/mcp_credentials.rs @@ -0,0 +1,131 @@ +use engram_coordinator::org_secrets::{seal_org_secret, OrgSecretBackend}; +use engram_core::traits::{ + LayeredSecretStore, MetadataStore, SecretContext, SecretStore, StaticSecretStore, +}; +use engram_core::types::org_secret::McpSecretRole; +use engram_core::types::SecretSchema; +use engram_core::SecretError; +use engram_crypto::EnvVarKeyProvider; +use engram_sim::{ManualClock, SimEntropy, SimMetadataStore}; +use std::collections::HashMap; +use std::sync::Arc; + +#[tokio::test] +async fn reserved_refs_deny_aliases_fallbacks_and_overrides_before_value_lookup() { + let clock = ManualClock::new(); + let meta = SimMetadataStore::new(clock, Arc::new(SimEntropy::seeded(4))); + let kek = Arc::new(EnvVarKeyProvider::from_bytes([7; 32], "test")); + let r = meta + .reserve_mcp_org_secret("reserved", "connection-a", McpSecretRole::ApiKey) + .await + .unwrap(); + let store = LayeredSecretStore::new(vec![ + Arc::new(OrgSecretBackend::new(meta.clone(), kek.clone())), + Arc::new(StaticSecretStore::new(HashMap::from([( + "reserved".into(), + "fallback-value".into(), + )]))), + ]); + let ctx = SecretContext { + repo: "", + image_tag: "", + }; + for present in [false, true] { + if present { + meta.put_mcp_org_secret( + seal_org_secret(kek.as_ref(), "reserved", b"private") + .await + .unwrap(), + &r, + ) + .await + .unwrap(); + } + for (name, reference) in [("reserved", None), ("alias", Some("reserved"))] { + let schema = SecretSchema { + r#ref: reference.map(String::from), + ..Default::default() + }; + assert!(matches!( + store.get(&ctx, name, &schema).await, + Err(SecretError::Unauthorized(_)) + )); + assert!(matches!( + store + .resolve( + &ctx, + &HashMap::from([(name.into(), schema)]), + Some(&HashMap::from([(name.into(), "override".into())])) + ) + .await, + Err(SecretError::Unauthorized(_)) + )); + } + } +} + +#[tokio::test] +async fn legacy_mcp_bindings_cannot_fetch_or_update_guest_credentials() { + use engram_coordinator::oauth::{OAuthManager, OAuthServiceError}; + use engram_core::types::oauth::{ + NewSealedOAuthCredential, OAuthAccountMetadata, OAuthCredentialKey, OAuthSubjectKind, + SessionOAuthBinding, + }; + use engram_crypto::CredCipher; + let clock = ManualClock::new(); + let entropy = Arc::new(SimEntropy::seeded(4)); + let meta = SimMetadataStore::new(clock.clone(), entropy.clone()); + let kek = Arc::new(EnvVarKeyProvider::from_bytes([7; 32], "test")); + let key = OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: "connection-a".into(), + provider: "openai-codex".into(), + }; + let sealed = CredCipher::new(kek.as_ref()) + .seal(b"private-bundle") + .await + .unwrap(); + meta.put_oauth_credential( + NewSealedOAuthCredential { + key: key.clone(), + wrapped_dek: sealed.wrapped_dek, + nonce: sealed.nonce.to_vec(), + ciphertext: sealed.ciphertext, + key_id: sealed.key_id, + metadata: OAuthAccountMetadata::default(), + expires_at: None, + }, + None, + ) + .await + .unwrap(); + let sid = engram_core::SessionId::from(uuid::Uuid::from_u128(4)); + meta.with_db_mut(|db| { + db.session_oauth_bindings.insert( + sid, + SessionOAuthBinding { + session_id: sid, + key, + }, + ); + }); + let manager = OAuthManager::new( + meta, + kek, + clock, + entropy, + Arc::new(StaticSecretStore::new(HashMap::new())), + ); + assert!(matches!( + manager.fetch_session(sid).await, + Err(OAuthServiceError::BadRequest(_)) + )); + assert!(matches!( + manager.update_session(sid, 1, b"replacement").await, + Err(OAuthServiceError::BadRequest(_)) + )); + assert!(matches!( + manager.report_session_broken(sid, 1, "revoked").await, + Err(OAuthServiceError::BadRequest(_)) + )); +} diff --git a/crates/engram-core/src/traits/metadata.rs b/crates/engram-core/src/traits/metadata.rs index 8e06eeccd..37590f0dd 100644 --- a/crates/engram-core/src/traits/metadata.rs +++ b/crates/engram-core/src/traits/metadata.rs @@ -512,6 +512,36 @@ pub trait MetadataStore: Send + Sync { Ok(false) } + /// Reserve only a fresh ref. Ownership survives deletion of its value. + async fn reserve_mcp_org_secret( + &self, + _name: &str, + _connection_id: &str, + _role: crate::types::org_secret::McpSecretRole, + ) -> Result { + Err(MetaError::Conflict( + "MCP credentials are unavailable".into(), + )) + } + async fn get_mcp_org_secret_reservation( + &self, + _name: &str, + ) -> Result, MetaError> { + Err(MetaError::Conflict( + "MCP credential purpose is unavailable".into(), + )) + } + /// Store ciphertext only if ownership and generation still match. + async fn put_mcp_org_secret( + &self, + _sealed: crate::types::org_secret::SealedOrgSecret, + _expected: &crate::types::org_secret::McpSecretReservation, + ) -> Result { + Err(MetaError::Conflict( + "MCP credentials are unavailable".into(), + )) + } + // ---- ADR 0106: subject-scoped OAuth credentials and short-lived flows ---- /// Insert a credential when `expected_version` is `None`, or update it iff diff --git a/crates/engram-core/src/traits/secrets.rs b/crates/engram-core/src/traits/secrets.rs index 9cd0469ab..849484600 100644 --- a/crates/engram-core/src/traits/secrets.rs +++ b/crates/engram-core/src/traits/secrets.rs @@ -63,6 +63,12 @@ pub struct ResolvedSecret { #[async_trait] pub trait SecretStore: Send + Sync { + /// Check purpose before overrides, fallback, or reuse of a cached mint engine. + /// Stores with reserved references must reject generic use explicitly. + async fn check_access(&self, _name: &str, _schema: &SecretSchema) -> Result<(), SecretError> { + Ok(()) + } + /// Look up a single secret. Returns `Ok(None)` for "not present" /// (caller decides if that's fatal based on `schema.required`). /// `schema.ref` may be set, in which case the backend should @@ -94,6 +100,7 @@ pub trait SecretStore: Send + Sync { ) -> Result { let mut bundle = SecretBundle::default(); for (name, sch) in schema { + self.check_access(name, sch).await?; let override_value = overrides.and_then(|m| m.get(name)); let resolved = match override_value { Some(v) => Some(v.clone()), @@ -145,12 +152,20 @@ impl LayeredSecretStore { #[async_trait] impl SecretStore for LayeredSecretStore { + async fn check_access(&self, name: &str, schema: &SecretSchema) -> Result<(), SecretError> { + for backend in &self.backends { + backend.check_access(name, schema).await?; + } + Ok(()) + } + async fn get( &self, ctx: &SecretContext<'_>, name: &str, schema: &SecretSchema, ) -> Result, SecretError> { + self.check_access(name, schema).await?; for backend in &self.backends { if let Some(value) = backend.get(ctx, name, schema).await? { return Ok(Some(value)); diff --git a/crates/engram-core/src/types/org_secret.rs b/crates/engram-core/src/types/org_secret.rs index 5f1101336..423b8b91f 100644 --- a/crates/engram-core/src/types/org_secret.rs +++ b/crates/engram-core/src/types/org_secret.rs @@ -35,3 +35,28 @@ pub struct OrgSecret { pub created_at: DateTime, pub updated_at: DateTime, } + +/// The only two uses for reserved organization credentials. +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum McpSecretRole { + ApiKey, + ClientSecret, +} +impl McpSecretRole { + pub fn as_str(self) -> &'static str { + match self { + Self::ApiKey => "api_key", + Self::ClientSecret => "client_secret", + } + } +} + +/// Permanent ownership, with a fence that changes on every value mutation. +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct McpSecretReservation { + pub name: String, + pub connection_id: String, + pub role: McpSecretRole, + pub generation: i64, +} diff --git a/crates/engram-postgres/src/lib.rs b/crates/engram-postgres/src/lib.rs index e884a8c7a..91da3ce1c 100644 --- a/crates/engram-postgres/src/lib.rs +++ b/crates/engram-postgres/src/lib.rs @@ -1579,6 +1579,14 @@ impl MetadataStore for PostgresStore { candidates: &[HostId], affinity_len: usize, ) -> Result { + if ws.oauth_binding.as_ref().is_some_and(|binding| { + binding.key.subject_kind == engram_core::types::oauth::OAuthSubjectKind::Mcp + }) { + return Err(MetaError::Conflict( + "MCP credentials cannot be bound to a guest session".into(), + )); + } + let now = self.clock.now_utc(); let mut tx = self.pool.begin().await.map_err(db_err)?; @@ -1855,6 +1863,22 @@ impl MetadataStore for PostgresStore { &self, sealed: engram_core::types::org_secret::SealedOrgSecret, ) -> Result { + let mut tx = self.pool.begin().await.map_err(db_err)?; + sqlx::query("INSERT INTO org_secret_purposes (name) VALUES ($1) ON CONFLICT DO NOTHING") + .bind(&sealed.name) + .execute(&mut *tx) + .await + .map_err(db_err)?; + let owner: Option = sqlx::query_scalar( + "SELECT connection_id FROM org_secret_purposes WHERE name=$1 FOR UPDATE", + ) + .bind(&sealed.name) + .fetch_one(&mut *tx) + .await + .map_err(db_err)?; + if owner.is_some() { + return Err(MetaError::Conflict("credential is reserved for MCP".into())); + } let (name, key_id, created_at, updated_at): ( String, String, @@ -1862,8 +1886,8 @@ impl MetadataStore for PostgresStore { chrono::DateTime, ) = sqlx::query_as( r#" - INSERT INTO org_secrets (name, wrapped_dek, nonce, ciphertext, key_id, updated_at) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO org_secrets (name, wrapped_dek, nonce, ciphertext, key_id, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $6) ON CONFLICT (name) DO UPDATE SET wrapped_dek = EXCLUDED.wrapped_dek, nonce = EXCLUDED.nonce, @@ -1879,9 +1903,10 @@ impl MetadataStore for PostgresStore { .bind(&sealed.ciphertext) .bind(&sealed.key_id) .bind(self.clock.now_utc()) - .fetch_one(&self.pool) + .fetch_one(&mut *tx) .await .map_err(db_err)?; + tx.commit().await.map_err(db_err)?; // Wake every replica's mint broker so a rotated cred rebuilds its // engine (ADR 0057 C2). Best-effort: the value is already committed, // so a notify failure must not fail the write. @@ -1947,12 +1972,23 @@ impl MetadataStore for PostgresStore { } async fn delete_org_secret(&self, name: &str) -> Result { + let mut tx = self.pool.begin().await.map_err(db_err)?; + sqlx::query("SELECT name FROM org_secret_purposes WHERE name=$1 FOR UPDATE") + .bind(name) + .execute(&mut *tx) + .await + .map_err(db_err)?; let res = sqlx::query("DELETE FROM org_secrets WHERE name = $1") .bind(name) - .execute(&self.pool) + .execute(&mut *tx) .await .map_err(db_err)?; let deleted = res.rows_affected() > 0; + if deleted { + sqlx::query("UPDATE org_secret_purposes SET generation=generation+1 WHERE name=$1 AND connection_id IS NOT NULL") + .bind(name).execute(&mut *tx).await.map_err(db_err)?; + } + tx.commit().await.map_err(db_err)?; if deleted { let _ = sqlx::query("SELECT pg_notify('org_secret_changed', $1)") .bind(name) @@ -1962,6 +1998,89 @@ impl MetadataStore for PostgresStore { Ok(deleted) } + async fn reserve_mcp_org_secret( + &self, + name: &str, + connection_id: &str, + role: engram_core::types::org_secret::McpSecretRole, + ) -> Result { + if name.is_empty() || connection_id.is_empty() { + return Err(MetaError::Conflict("invalid MCP reservation".into())); + } + let mut tx = self.pool.begin().await.map_err(db_err)?; + sqlx::query("INSERT INTO org_secret_purposes (name, connection_id, role) VALUES ($1,$2,$3) ON CONFLICT DO NOTHING") + .bind(name).bind(connection_id).bind(role.as_str()).execute(&mut *tx).await.map_err(db_err)?; + let (owner, stored_role, generation): (Option, Option, i64) = sqlx::query_as("SELECT connection_id, role, generation FROM org_secret_purposes WHERE name=$1 FOR UPDATE") + .bind(name).fetch_one(&mut *tx).await.map_err(db_err)?; + if owner.as_deref() != Some(connection_id) || stored_role.as_deref() != Some(role.as_str()) + { + return Err(MetaError::Conflict("credential purpose conflict".into())); + } + sqlx::query("SELECT pg_notify('org_secret_changed', $1)") + .bind(name) + .execute(&mut *tx) + .await + .map_err(db_err)?; + tx.commit().await.map_err(db_err)?; + Ok(engram_core::types::org_secret::McpSecretReservation { + name: name.into(), + connection_id: connection_id.into(), + role, + generation, + }) + } + + async fn get_mcp_org_secret_reservation( + &self, + name: &str, + ) -> Result, MetaError> { + use engram_core::types::org_secret::{McpSecretReservation, McpSecretRole}; + let row: Option<(String,String,i64)> = sqlx::query_as("SELECT connection_id, role, generation FROM org_secret_purposes WHERE name=$1 AND connection_id IS NOT NULL") + .bind(name).fetch_optional(&self.pool).await.map_err(db_err)?; + row.map(|(connection_id, role, generation)| { + Ok(McpSecretReservation { + name: name.into(), + connection_id, + role: match role.as_str() { + "api_key" => McpSecretRole::ApiKey, + "client_secret" => McpSecretRole::ClientSecret, + _ => return Err(MetaError::Conflict("invalid credential role".into())), + }, + generation, + }) + }) + .transpose() + } + + async fn put_mcp_org_secret( + &self, + sealed: engram_core::types::org_secret::SealedOrgSecret, + expected: &engram_core::types::org_secret::McpSecretReservation, + ) -> Result { + if sealed.name != expected.name { + return Err(MetaError::Conflict("credential reservation changed".into())); + } + let mut tx = self.pool.begin().await.map_err(db_err)?; + let generation: Option = sqlx::query_scalar("UPDATE org_secret_purposes SET generation=generation+1 WHERE name=$1 AND connection_id=$2 AND role=$3 AND generation=$4 RETURNING generation") + .bind(&sealed.name).bind(&expected.connection_id).bind(expected.role.as_str()).bind(expected.generation) + .fetch_optional(&mut *tx).await.map_err(db_err)?; + let generation = generation + .ok_or_else(|| MetaError::Conflict("credential reservation changed".into()))?; + sqlx::query("INSERT INTO org_secrets (name,wrapped_dek,nonce,ciphertext,key_id,created_at,updated_at) VALUES ($1,$2,$3,$4,$5,$6,$6) ON CONFLICT (name) DO UPDATE SET wrapped_dek=$2,nonce=$3,ciphertext=$4,key_id=$5,updated_at=$6") + .bind(&sealed.name).bind(&sealed.wrapped_dek).bind(&sealed.nonce).bind(&sealed.ciphertext).bind(&sealed.key_id).bind(self.clock.now_utc()) + .execute(&mut *tx).await.map_err(db_err)?; + sqlx::query("SELECT pg_notify('org_secret_changed', $1)") + .bind(&sealed.name) + .execute(&mut *tx) + .await + .map_err(db_err)?; + tx.commit().await.map_err(db_err)?; + Ok(engram_core::types::org_secret::McpSecretReservation { + generation, + ..expected.clone() + }) + } + // ---- ADR 0106: subject-scoped OAuth credentials and flow state ---- async fn put_oauth_credential( diff --git a/crates/engram-sim/src/meta/mod.rs b/crates/engram-sim/src/meta/mod.rs index 6bb3e86f1..48721d86e 100644 --- a/crates/engram-sim/src/meta/mod.rs +++ b/crates/engram-sim/src/meta/mod.rs @@ -222,6 +222,17 @@ pub struct SimDb { /// `session_broker_tokens`, PK `session_id`). pub broker_tokens: std::collections::BTreeMap, + pub org_secret_purposes: std::collections::BTreeMap< + String, + Option, + >, + pub org_secrets: std::collections::BTreeMap< + String, + ( + engram_core::types::org_secret::SealedOrgSecret, + engram_core::types::org_secret::OrgSecret, + ), + >, /// ADR 0106: sealed payloads keyed by opaque subject + provider. pub oauth_credentials: std::collections::BTreeMap< engram_core::types::oauth::OAuthCredentialKey, diff --git a/crates/engram-sim/src/meta/store_impl.rs b/crates/engram-sim/src/meta/store_impl.rs index d605876e4..e98ff696e 100644 --- a/crates/engram-sim/src/meta/store_impl.rs +++ b/crates/engram-sim/src/meta/store_impl.rs @@ -817,6 +817,14 @@ impl MetadataStore for SimMetadataStore { candidates: &[HostId], affinity_len: usize, ) -> Result { + if ws.oauth_binding.as_ref().is_some_and(|binding| { + binding.key.subject_kind == engram_core::types::oauth::OAuthSubjectKind::Mcp + }) { + return Err(MetaError::Conflict( + "MCP credentials cannot be bound to a guest session".into(), + )); + } + self.gate()?; let now = self.now(); let mut db = self.db.lock(); @@ -4487,8 +4495,20 @@ impl MetadataStore for SimMetadataStore { Ok(DeleteHostOutcome::Deleted) } - async fn delete_org_secret(&self, _name: &str) -> Result { - panic!("SimMeta: delete_org_secret not implemented — add it plus a conformance case (ADR 0098 D4)") + async fn delete_org_secret(&self, name: &str) -> Result { + self.gate()?; + let mut db = self.db.lock(); + let deleted = db.org_secrets.remove(name).is_some(); + if deleted { + if let Some(Some(reservation)) = db.org_secret_purposes.get_mut(name) { + reservation.generation += 1; + } + } + drop(db); + if deleted { + self.notify("org_secret_changed", name.to_string()); + } + Ok(deleted) } async fn expire_capture_job_stages( @@ -4559,9 +4579,15 @@ impl MetadataStore for SimMetadataStore { async fn get_org_secret_sealed( &self, - _name: &str, + name: &str, ) -> Result, MetaError> { - panic!("SimMeta: get_org_secret_sealed not implemented — add it plus a conformance case (ADR 0098 D4)") + self.gate()?; + Ok(self + .db + .lock() + .org_secrets + .get(name) + .map(|(sealed, _)| sealed.clone())) } async fn get_oauth_credential( @@ -4836,7 +4862,14 @@ impl MetadataStore for SimMetadataStore { async fn list_org_secrets( &self, ) -> Result, MetaError> { - panic!("SimMeta: list_org_secrets not implemented — add it plus a conformance case (ADR 0098 D4)") + self.gate()?; + Ok(self + .db + .lock() + .org_secrets + .values() + .map(|(_, meta)| meta.clone()) + .collect()) } /// `SELECT prestage_ref FROM enable_jobs WHERE state='prestaging'` — @@ -5516,9 +5549,109 @@ impl MetadataStore for SimMetadataStore { async fn upsert_org_secret( &self, - _sealed: engram_core::types::org_secret::SealedOrgSecret, + sealed: engram_core::types::org_secret::SealedOrgSecret, ) -> Result { - panic!("SimMeta: upsert_org_secret not implemented — add it plus a conformance case (ADR 0098 D4)") + self.gate()?; + let now = self.now(); + let mut db = self.db.lock(); + if db + .org_secret_purposes + .get(&sealed.name) + .is_some_and(Option::is_some) + { + return Err(MetaError::Conflict("credential is reserved for MCP".into())); + } + db.org_secret_purposes.insert(sealed.name.clone(), None); + let meta = engram_core::types::org_secret::OrgSecret { + name: sealed.name.clone(), + key_id: sealed.key_id.clone(), + created_at: db + .org_secrets + .get(&sealed.name) + .map_or(now, |(_, row)| row.created_at), + updated_at: now, + }; + db.org_secrets + .insert(sealed.name.clone(), (sealed, meta.clone())); + drop(db); + self.notify("org_secret_changed", meta.name.clone()); + Ok(meta) + } + + async fn reserve_mcp_org_secret( + &self, + name: &str, + connection_id: &str, + role: engram_core::types::org_secret::McpSecretRole, + ) -> Result { + self.gate()?; + let mut db = self.db.lock(); + if name.is_empty() || connection_id.is_empty() { + return Err(MetaError::Conflict("invalid MCP reservation".into())); + } + if let Some(current) = db.org_secret_purposes.get(name) { + return match current { + Some(r) if r.connection_id == connection_id && r.role == role => Ok(r.clone()), + _ => Err(MetaError::Conflict("credential purpose conflict".into())), + }; + } + let reservation = engram_core::types::org_secret::McpSecretReservation { + name: name.into(), + connection_id: connection_id.into(), + role, + generation: 0, + }; + db.org_secret_purposes + .insert(name.into(), Some(reservation.clone())); + drop(db); + self.notify("org_secret_changed", name.to_string()); + Ok(reservation) + } + + async fn get_mcp_org_secret_reservation( + &self, + name: &str, + ) -> Result, MetaError> { + self.gate()?; + Ok(self + .db + .lock() + .org_secret_purposes + .get(name) + .cloned() + .flatten()) + } + + async fn put_mcp_org_secret( + &self, + sealed: engram_core::types::org_secret::SealedOrgSecret, + expected: &engram_core::types::org_secret::McpSecretReservation, + ) -> Result { + self.gate()?; + let now = self.now(); + let mut db = self.db.lock(); + if sealed.name != expected.name + || db.org_secret_purposes.get(&sealed.name) != Some(&Some(expected.clone())) + { + return Err(MetaError::Conflict("credential reservation changed".into())); + } + let mut next = expected.clone(); + next.generation += 1; + let meta = engram_core::types::org_secret::OrgSecret { + name: sealed.name.clone(), + key_id: sealed.key_id.clone(), + created_at: db + .org_secrets + .get(&sealed.name) + .map_or(now, |(_, row)| row.created_at), + updated_at: now, + }; + db.org_secrets.insert(sealed.name.clone(), (sealed, meta)); + db.org_secret_purposes + .insert(next.name.clone(), Some(next.clone())); + drop(db); + self.notify("org_secret_changed", next.name.clone()); + Ok(next) } async fn put_oauth_credential( diff --git a/crates/engram-sim/tests/meta_conformance.rs b/crates/engram-sim/tests/meta_conformance.rs index feb64843b..0d490d898 100644 --- a/crates/engram-sim/tests/meta_conformance.rs +++ b/crates/engram-sim/tests/meta_conformance.rs @@ -7373,3 +7373,211 @@ async fn nbd_capture_placement(ctx: &Ctx) { ); } conformance!(t_nbd_capture_placement, super::nbd_capture_placement); + +async fn mcp_org_secret_clock(ctx: &Ctx) { + use engram_core::types::org_secret::SealedOrgSecret; + let value = SealedOrgSecret { + name: "fresh-key".into(), + wrapped_dek: vec![1], + nonce: vec![0; 12], + ciphertext: vec![2], + key_id: "test".into(), + }; + let row = ctx.meta.upsert_org_secret(value.clone()).await.unwrap(); + assert_eq!(row.created_at, ctx.clock.now_utc()); + ctx.clock.advance(Duration::from_secs(5)); + let rotated = ctx.meta.upsert_org_secret(value).await.unwrap(); + assert_eq!(rotated.created_at, row.created_at); + assert_eq!(rotated.updated_at, ctx.clock.now_utc()); + assert_eq!(ctx.meta.list_org_secrets().await.unwrap().len(), 1); + assert_eq!( + ctx.meta + .get_org_secret_sealed("fresh-key") + .await + .unwrap() + .unwrap() + .ciphertext, + vec![2] + ); + assert!(ctx.meta.delete_org_secret("fresh-key").await.unwrap()); + assert!(!ctx.meta.delete_org_secret("fresh-key").await.unwrap()); + assert!(ctx + .meta + .get_org_secret_sealed("fresh-key") + .await + .unwrap() + .is_none()); +} +conformance!(t_mcp_org_secret_clock, super::mcp_org_secret_clock); + +async fn mcp_org_reservation(ctx: &Ctx) { + use engram_core::types::org_secret::{McpSecretRole, SealedOrgSecret}; + let sealed = |name: &str| SealedOrgSecret { + name: name.into(), + wrapped_dek: vec![1], + nonce: vec![0; 12], + ciphertext: vec![2], + key_id: "test".into(), + }; + let reserved = ctx + .meta + .reserve_mcp_org_secret("reserved", "connection-a", McpSecretRole::ApiKey) + .await + .unwrap(); + assert_eq!(reserved.generation, 0); + assert_eq!( + ctx.meta + .reserve_mcp_org_secret("reserved", "connection-a", McpSecretRole::ApiKey) + .await + .unwrap(), + reserved + ); + assert!(ctx + .meta + .reserve_mcp_org_secret("reserved", "connection-b", McpSecretRole::ApiKey) + .await + .is_err()); + assert!(ctx + .meta + .reserve_mcp_org_secret("reserved", "connection-a", McpSecretRole::ClientSecret) + .await + .is_err()); + assert!(ctx + .meta + .upsert_org_secret(sealed("reserved")) + .await + .is_err()); + let first = ctx + .meta + .put_mcp_org_secret(sealed("reserved"), &reserved) + .await + .unwrap(); + assert_eq!(first.generation, 1); + assert!(ctx + .meta + .put_mcp_org_secret(sealed("reserved"), &reserved) + .await + .is_err()); + assert!(ctx.meta.delete_org_secret("reserved").await.unwrap()); + let deleted = ctx + .meta + .get_mcp_org_secret_reservation("reserved") + .await + .unwrap() + .unwrap(); + assert_eq!(deleted.generation, 2); + assert!(ctx + .meta + .get_org_secret_sealed("reserved") + .await + .unwrap() + .is_none()); + assert!(ctx + .meta + .put_mcp_org_secret(sealed("reserved"), &first) + .await + .is_err()); + assert_eq!( + ctx.meta + .put_mcp_org_secret(sealed("reserved"), &deleted) + .await + .unwrap() + .generation, + 3 + ); + let concurrent = ctx + .meta + .reserve_mcp_org_secret("concurrent", "connection-a", McpSecretRole::ApiKey) + .await + .unwrap(); + let (a, b) = tokio::join!( + ctx.meta + .put_mcp_org_secret(sealed("concurrent"), &concurrent), + ctx.meta + .put_mcp_org_secret(sealed("concurrent"), &concurrent), + ); + assert_eq!(usize::from(a.is_ok()) + usize::from(b.is_ok()), 1); + assert_eq!( + ctx.meta + .get_mcp_org_secret_reservation("concurrent") + .await + .unwrap() + .unwrap() + .generation, + 1 + ); + ctx.meta.upsert_org_secret(sealed("exposed")).await.unwrap(); + ctx.meta.delete_org_secret("exposed").await.unwrap(); + assert!(ctx + .meta + .reserve_mcp_org_secret("exposed", "connection-a", McpSecretRole::ApiKey) + .await + .is_err()); +} +conformance!(t_mcp_org_reservation, super::mcp_org_reservation); + +async fn mcp_guest_binding_rejected(ctx: &Ctx) { + use engram_core::types::oauth::{ + NewSealedOAuthCredential, OAuthAccountMetadata, OAuthCredentialKey, OAuthSubjectKind, + SessionOAuthBinding, + }; + let key = OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: "connection-a".into(), + provider: "mcp".into(), + }; + ctx.meta + .put_oauth_credential( + NewSealedOAuthCredential { + key: key.clone(), + wrapped_dek: vec![1], + nonce: vec![0; 12], + ciphertext: vec![2], + key_id: "test".into(), + metadata: OAuthAccountMetadata::default(), + expires_at: None, + }, + None, + ) + .await + .unwrap(); + let sid = SessionId::from(uuid::Uuid::from_u128(4)); + let ws = engram_core::traits::SessionCreateWriteSet { + nbd_slot_need: 0, + session_id: sid, + spec: spec("test:mcp"), + mem_budget_mib: 1024, + cpu_budget_vcpus: 1, + sealed_secrets: None, + capabilities: vec![], + integration_policy_json: None, + runtime_spec: engram_core::types::runtime_spec::RuntimeSpec::new( + vec![], + None, + None, + vec![], + ), + oauth_binding: Some(SessionOAuthBinding { + session_id: sid, + key, + }), + }; + assert!(matches!( + ctx.meta.reserve_and_persist_create(ws, &[], 0).await, + Err(MetaError::Conflict(_)) + )); + assert!(ctx + .meta + .get_session_oauth_binding(sid) + .await + .unwrap() + .is_none()); + assert!(matches!( + ctx.meta.get_session(sid).await, + Err(MetaError::NotFound) + )); +} +conformance!( + t_mcp_guest_binding_rejected, + super::mcp_guest_binding_rejected +); diff --git a/deploy/migrations/0126_mcp_secret_reservations.sql b/deploy/migrations/0126_mcp_secret_reservations.sql new file mode 100644 index 000000000..d56cdcc36 --- /dev/null +++ b/deploy/migrations/0126_mcp_secret_reservations.sql @@ -0,0 +1,10 @@ +-- Keep purpose after value deletion. A generic name can never become an MCP ref. +CREATE TABLE org_secret_purposes ( + name text PRIMARY KEY, + connection_id text, + role text CHECK (role IN ('api_key', 'client_secret')), + generation bigint NOT NULL DEFAULT 0 CHECK (generation >= 0), + CHECK ((connection_id IS NULL) = (role IS NULL)), + CHECK (connection_id IS NULL OR length(connection_id) > 0) +); +INSERT INTO org_secret_purposes (name) SELECT name FROM org_secrets; From 17adcf01aa7b515186ec3f8adc4df9a23f2901da Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 23:06:36 -0500 Subject: [PATCH 009/279] Fence generic secret reads after credential publication --- crates/engram-coordinator/src/org_secrets.rs | 11 +- .../tests/mcp_credentials.rs | 140 ++++++++++++++++++ 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/crates/engram-coordinator/src/org_secrets.rs b/crates/engram-coordinator/src/org_secrets.rs index 8fefebe49..36d6c0029 100644 --- a/crates/engram-coordinator/src/org_secrets.rs +++ b/crates/engram-coordinator/src/org_secrets.rs @@ -72,12 +72,17 @@ impl SecretStore for OrgSecretBackend { schema: &SecretSchema, ) -> Result, SecretError> { self.check_access(name, schema).await?; - let Some(sealed) = self + let sealed = self .meta .get_org_secret_sealed(name) .await - .map_err(|e| SecretError::Backend(Box::new(e)))? - else { + .map_err(|e| SecretError::Backend(Box::new(e)))?; + // A fresh name can be reserved and published after the first check. + // Purpose is permanent, and these are current reads from the same + // store: any MCP payload observed above must have a reservation here. + // Check absent payloads too, before allowing a deployment fallback. + self.check_access(name, schema).await?; + let Some(sealed) = sealed else { return Ok(None); }; let cred = to_sealed_cred(&sealed)?; diff --git a/crates/engram-coordinator/tests/mcp_credentials.rs b/crates/engram-coordinator/tests/mcp_credentials.rs index 8fab6ed5c..ca75b1b6c 100644 --- a/crates/engram-coordinator/tests/mcp_credentials.rs +++ b/crates/engram-coordinator/tests/mcp_credentials.rs @@ -129,3 +129,143 @@ async fn legacy_mcp_bindings_cannot_fetch_or_update_guest_credentials() { Err(OAuthServiceError::BadRequest(_)) )); } + +mod publication_race { + use super::*; + use engram_core::traits::{ + CreateDisposition, DisableEnabledImageOutcome, SessionCreateWriteSet, + }; + use engram_core::types::event::{ArtifactRow, EventCursor, PersistedEvent}; + use engram_core::types::host::{HostHeartbeat, HostRecord, HostStatus}; + use engram_core::types::registry::{EnabledImage, RegistryCredential, SessionSecrets}; + use engram_core::types::session::{BindingDisposition, Session, SessionSpec, SessionState}; + use engram_core::types::snapshot::SnapshotRecord; + use engram_core::{HostId, MetaError, SandboxId, SessionId}; + use tokio::sync::Notify; + + // Forward to the real simulator. Only the payload-read boundary is paused; + // reservation, ciphertext publication, and all purpose reads keep real behavior. + struct PausedMeta { + inner: Arc, + read_started: Notify, + resume_read: Notify, + } + + macro_rules! delegate_required { + ($( $method:ident ( $( $arg:ident: $ty:ty ),* $(,)? ) -> $ret:ty; )*) => { + #[async_trait::async_trait] + impl MetadataStore for PausedMeta { + async fn get_mcp_org_secret_reservation(&self, name: &str) -> Result, MetaError> { + self.inner.get_mcp_org_secret_reservation(name).await + } + async fn get_org_secret_sealed(&self, name: &str) -> Result, MetaError> { + self.read_started.notify_one(); + self.resume_read.notified().await; + self.inner.get_org_secret_sealed(name).await + } + $(async fn $method(&self, $( $arg: $ty ),*) -> $ret { + self.inner.$method($( $arg ),*).await + })* + } + } + } + + delegate_required! { + create_session(spec: SessionSpec) -> Result; + transition_session_created(session_id: SessionId, sandbox_id: SandboxId,) -> Result; + get_session(id: SessionId) -> Result; + list_active_sessions() -> Result, MetaError>; + reserve_and_persist_create(ws: SessionCreateWriteSet, candidates: &[HostId], affinity_len: usize,) -> Result; + transition_session(id: SessionId, target: SessionState, disposition: BindingDisposition,) -> Result; + assign_session_host(id: SessionId, host_id: Option,) -> Result<(), MetaError>; + upsert_host(host: HostRecord) -> Result<(), MetaError>; + list_active_hosts() -> Result, MetaError>; + set_host_status(id: HostId, status: HostStatus) -> Result<(), MetaError>; + touch_host_heartbeat(id: HostId, hb: HostHeartbeat,) -> Result; + set_host_cordon(id: HostId, owner: Option, reason: Option<&str>,) -> Result<(), MetaError>; + mark_host_dead_if_lease_expired(host_id: HostId,) -> Result, MetaError>; + record_snapshot(snap: SnapshotRecord) -> Result; + list_snapshots_for_session(sid: SessionId,) -> Result, MetaError>; + latest_snapshot_for_session(sid: SessionId,) -> Result, MetaError>; + append_session_event(session_id: SessionId, kind: &str, payload: serde_json::Value,) -> Result; + list_session_events_window(session_id: SessionId, cursor: EventCursor, limit: i64, kinds: &[String], tool_names: &[String],) -> Result, MetaError>; + insert_artifact(id: uuid::Uuid, session_id: SessionId, blob_key: &str, media_type: &str, size_bytes: i64, caption: Option<&str>, file_name: Option<&str>,) -> Result<(), MetaError>; + get_artifact(session_id: SessionId, id: uuid::Uuid,) -> Result, MetaError>; + artifact_usage(session_id: SessionId) -> Result<(i64, i64), MetaError>; + upsert_registry_credential(cred: RegistryCredential) -> Result<(), MetaError>; + list_registry_credentials() -> Result, MetaError>; + registry_credential_for_host(registry_host: &str,) -> Result, MetaError>; + delete_registry_credential(registry_host: &str) -> Result<(), MetaError>; + upsert_enabled_image(image: EnabledImage) -> Result<(), MetaError>; + list_enabled_images() -> Result, MetaError>; + get_enabled_image(image_uri: &str) -> Result, MetaError>; + get_enabled_image_any(image_uri: &str,) -> Result, MetaError>; + soft_delete_enabled_image(image_uri: &str,) -> Result; + delete_enabled_image(image_uri: &str) -> Result<(), MetaError>; + get_session_secrets(session_id: SessionId,) -> Result, MetaError>; + delete_session_secrets(session_id: SessionId) -> Result<(), MetaError>; + } + + #[tokio::test] + async fn reservation_published_after_initial_check_blocks_payload_and_fallback() { + for publish_value in [true, false] { + let meta = SimMetadataStore::new(ManualClock::new(), Arc::new(SimEntropy::seeded(4))); + let paused = Arc::new(PausedMeta { + inner: meta.clone(), + read_started: Notify::new(), + resume_read: Notify::new(), + }); + let kek = Arc::new(EnvVarKeyProvider::from_bytes([7; 32], "test")); + let store = LayeredSecretStore::new(vec![ + Arc::new(OrgSecretBackend::new(paused.clone(), kek.clone())), + Arc::new(StaticSecretStore::new(HashMap::from([( + "fresh".into(), + "fallback-value".into(), + )]))), + ]); + let reader = tokio::spawn(async move { + store + .get( + &SecretContext { + repo: "", + image_tag: "", + }, + "fresh", + &SecretSchema::default(), + ) + .await + }); + tokio::time::timeout( + std::time::Duration::from_secs(5), + paused.read_started.notified(), + ) + .await + .unwrap(); + // The initial generic checks have returned no reservation. The read + // is now suspended before it can observe the newly published value. + let reserved = meta + .reserve_mcp_org_secret("fresh", "connection-a", McpSecretRole::ApiKey) + .await + .unwrap(); + if publish_value { + meta.put_mcp_org_secret( + seal_org_secret(kek.as_ref(), "fresh", b"private-value") + .await + .unwrap(), + &reserved, + ) + .await + .unwrap(); + } + paused.resume_read.notify_one(); + let result = tokio::time::timeout(std::time::Duration::from_secs(5), reader) + .await + .unwrap() + .unwrap(); + assert!( + matches!(result, Err(SecretError::Unauthorized(_))), + "a reservation published during a generic read must block plaintext and fallback" + ); + } + } +} From 51ae1760c58063336493928240325d957c795a69 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 23:52:27 -0500 Subject: [PATCH 010/279] Add sealed MCP OAuth state and exclusive refresh claims --- crates/engram-core/src/traits/metadata.rs | 46 ++++ crates/engram-core/src/types/mcp.rs | 25 ++ crates/engram-core/src/types/mod.rs | 1 + crates/engram-core/src/types/oauth.rs | 3 +- crates/engram-postgres/src/lib.rs | 112 ++++++++ crates/engram-sim/src/meta/mod.rs | 8 + crates/engram-sim/src/meta/store_impl.rs | 158 ++++++++++++ crates/engram-sim/tests/meta_conformance.rs | 239 ++++++++++++++++++ .../migrations/0127_mcp_oauth_authority.sql | 14 + docs/adr/0125-isolated-external-mcp.md | 26 ++ 10 files changed, 631 insertions(+), 1 deletion(-) create mode 100644 crates/engram-core/src/types/mcp.rs create mode 100644 deploy/migrations/0127_mcp_oauth_authority.sql diff --git a/crates/engram-core/src/traits/metadata.rs b/crates/engram-core/src/traits/metadata.rs index 37590f0dd..2d1feae83 100644 --- a/crates/engram-core/src/traits/metadata.rs +++ b/crates/engram-core/src/traits/metadata.rs @@ -582,6 +582,52 @@ pub trait MetadataStore: Send + Sync { Err(MetaError::NotFound) } + /// Exclusive refresh dispatch. Marks the current token unavailable immediately; + /// an expired or uncertain claim cannot be reacquired without reconnect. + async fn claim_mcp_refresh( + &self, + _claim: &crate::types::mcp::McpRefreshClaim, + ) -> Result { + Err(MetaError::Conflict("MCP refresh unavailable".into())) + } + async fn complete_mcp_refresh( + &self, + _credential: crate::types::oauth::NewSealedOAuthCredential, + _claim: &crate::types::mcp::McpRefreshClaim, + ) -> Result { + Err(MetaError::Conflict("MCP refresh unavailable".into())) + } + /// Four deployment slots and one slot per connection, shared by replicas. + async fn acquire_mcp_oauth_admission( + &self, + _lease: &crate::types::mcp::McpOAuthAdmission, + ) -> Result { + Err(MetaError::Conflict("MCP admission unavailable".into())) + } + async fn release_mcp_oauth_admission( + &self, + _lease: &crate::types::mcp::McpOAuthAdmission, + ) -> Result<(), MetaError> { + Err(MetaError::Conflict("MCP admission unavailable".into())) + } + + /// Create encrypted MCP redirect state atomically with its metadata row. + async fn create_mcp_oauth_flow( + &self, + _flow: crate::types::oauth::OAuthFlow, + _state: crate::types::mcp::McpSealedState, + ) -> Result<(), MetaError> { + Err(MetaError::Conflict("MCP flow storage unavailable".into())) + } + /// Remove and return state exactly once, before any token exchange. + async fn consume_mcp_oauth_flow( + &self, + _id: uuid::Uuid, + _key: &crate::types::oauth::OAuthCredentialKey, + ) -> Result { + Err(MetaError::Conflict("MCP flow storage unavailable".into())) + } + async fn create_oauth_flow( &self, _flow: crate::types::oauth::OAuthFlow, diff --git a/crates/engram-core/src/types/mcp.rs b/crates/engram-core/src/types/mcp.rs new file mode 100644 index 000000000..4886762cc --- /dev/null +++ b/crates/engram-core/src/types/mcp.rs @@ -0,0 +1,25 @@ +//! Coordinator-owned MCP persistence records. No plaintext credential fields. +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct McpSealedState { + pub wrapped_dek: Vec, + pub nonce: Vec, + pub ciphertext: Vec, + pub key_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct McpRefreshClaim { + pub id: uuid::Uuid, + pub key: super::oauth::OAuthCredentialKey, + pub version: i64, + pub expires_at: chrono::DateTime, +} +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct McpOAuthAdmission { + pub id: uuid::Uuid, + pub deployment_id: String, + pub connection_id: String, + pub expires_at: chrono::DateTime, +} diff --git a/crates/engram-core/src/types/mod.rs b/crates/engram-core/src/types/mod.rs index 89a60afc0..1f7909c20 100644 --- a/crates/engram-core/src/types/mod.rs +++ b/crates/engram-core/src/types/mod.rs @@ -17,6 +17,7 @@ pub mod image; pub mod integration; pub mod manifest; pub mod materialize; +pub mod mcp; pub mod oauth; pub mod org_secret; pub mod outbox; diff --git a/crates/engram-core/src/types/oauth.rs b/crates/engram-core/src/types/oauth.rs index da2363d4d..5d928b1f7 100644 --- a/crates/engram-core/src/types/oauth.rs +++ b/crates/engram-core/src/types/oauth.rs @@ -68,7 +68,8 @@ pub struct SessionOAuthBinding { /// Provider-approved metadata safe to return to clients and logs. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct OAuthAccountMetadata { - /// Provider-stable identity used to reject account switching on refresh. + /// Authority identity used to reject account switching on refresh. MCP uses + /// an authorization-ceremony identity with sealed provider provenance. /// This value is not rendered by the UI. pub account_id: String, pub display_name: Option, diff --git a/crates/engram-postgres/src/lib.rs b/crates/engram-postgres/src/lib.rs index 91da3ce1c..e26dae0d7 100644 --- a/crates/engram-postgres/src/lib.rs +++ b/crates/engram-postgres/src/lib.rs @@ -2237,6 +2237,116 @@ impl MetadataStore for PostgresStore { } } + async fn claim_mcp_refresh( + &self, + claim: &engram_core::types::mcp::McpRefreshClaim, + ) -> Result { + let now = self.clock.now_utc(); + if claim.key.subject_kind != engram_core::types::oauth::OAuthSubjectKind::Mcp + || claim.expires_at <= now + || claim.expires_at > now + chrono::Duration::seconds(30) + { + return Err(MetaError::Conflict("invalid refresh claim".into())); + } + let r = sqlx::query("UPDATE oauth_credentials SET mcp_refresh_claim=$4, refresh_claim_until=$5, broken_at=$6, broken_reason='refresh_pending' WHERE subject_kind='mcp' AND subject_id=$1 AND provider=$2 AND version=$3 AND revoked_at IS NULL AND broken_at IS NULL") + .bind(&claim.key.subject_id).bind(&claim.key.provider).bind(claim.version).bind(claim.id).bind(claim.expires_at).bind(now).execute(&self.pool).await.map_err(db_err)?; + Ok(r.rows_affected() == 1) + } + async fn complete_mcp_refresh( + &self, + credential: engram_core::types::oauth::NewSealedOAuthCredential, + claim: &engram_core::types::mcp::McpRefreshClaim, + ) -> Result { + if credential.key != claim.key + || claim.key.subject_kind != engram_core::types::oauth::OAuthSubjectKind::Mcp + { + return Err(MetaError::Conflict("refresh claim lost".into())); + } + let metadata = serde_json::to_value(&credential.metadata) + .map_err(|e| MetaError::Serialization(e.to_string()))?; + let row = sqlx::query("UPDATE oauth_credentials SET wrapped_dek=$6,nonce=$7,ciphertext=$8,key_id=$9,account_metadata=$10,expires_at=$11,version=version+1,updated_at=$12,broken_at=NULL,broken_reason=NULL,refresh_claim_until=NULL,mcp_refresh_claim=NULL WHERE subject_kind='mcp' AND subject_id=$1 AND provider=$2 AND version=$3 AND mcp_refresh_claim=$4 AND refresh_claim_until=$5 AND refresh_claim_until>$12 AND revoked_at IS NULL AND broken_reason='refresh_pending' RETURNING *") + .bind(&claim.key.subject_id).bind(&claim.key.provider).bind(claim.version).bind(claim.id).bind(claim.expires_at) + .bind(credential.wrapped_dek).bind(credential.nonce).bind(credential.ciphertext).bind(credential.key_id).bind(metadata).bind(credential.expires_at).bind(self.clock.now_utc()).fetch_optional(&self.pool).await.map_err(db_err)?.ok_or_else(|| MetaError::Conflict("refresh claim lost".into()))?; + oauth_credential_from_pg(&row) + } + async fn acquire_mcp_oauth_admission( + &self, + lease: &engram_core::types::mcp::McpOAuthAdmission, + ) -> Result { + let now = self.clock.now_utc(); + if lease.deployment_id.is_empty() + || lease.connection_id.is_empty() + || lease.expires_at <= now + || lease.expires_at > now + chrono::Duration::seconds(30) + { + return Err(MetaError::Conflict("invalid OAuth admission".into())); + } + sqlx::query("DELETE FROM mcp_oauth_admission WHERE expires_at<=$1") + .bind(now) + .execute(&self.pool) + .await + .map_err(db_err)?; + // The fixed slot primary key and connection uniqueness bound concurrent replicas. + for slot in 0..4i16 { + let r = sqlx::query("INSERT INTO mcp_oauth_admission (deployment_id,slot,id,connection_id,expires_at) VALUES ($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING") + .bind(&lease.deployment_id).bind(slot).bind(lease.id).bind(&lease.connection_id).bind(lease.expires_at).execute(&self.pool).await.map_err(db_err)?; + if r.rows_affected() == 1 { + return Ok(true); + } + } + Ok(false) + } + async fn release_mcp_oauth_admission( + &self, + lease: &engram_core::types::mcp::McpOAuthAdmission, + ) -> Result<(), MetaError> { + sqlx::query("DELETE FROM mcp_oauth_admission WHERE id=$1 AND deployment_id=$2 AND connection_id=$3 AND expires_at=$4") + .bind(lease.id).bind(&lease.deployment_id).bind(&lease.connection_id).bind(lease.expires_at).execute(&self.pool).await.map_err(db_err)?; + Ok(()) + } + + async fn create_mcp_oauth_flow( + &self, + flow: engram_core::types::oauth::OAuthFlow, + state: engram_core::types::mcp::McpSealedState, + ) -> Result<(), MetaError> { + use engram_core::types::oauth::{OAuthFlowStatus, OAuthSubjectKind}; + if flow.key.subject_kind != OAuthSubjectKind::Mcp + || flow.status != OAuthFlowStatus::Pending + || flow.expires_at <= self.clock.now_utc() + { + return Err(MetaError::Conflict("MCP flow creation rejected".into())); + } + let state = + serde_json::to_value(state).map_err(|e| MetaError::Serialization(e.to_string()))?; + let result = sqlx::query("INSERT INTO oauth_flows (id,subject_kind,subject_id,provider,owner_replica,lease_expires_at,expires_at,status,created_at,updated_at,mcp_state) VALUES ($1,'mcp',$2,$3,$4,$5,$6,'pending',$7,$8,$9) ON CONFLICT DO NOTHING") + .bind(flow.id).bind(&flow.key.subject_id).bind(&flow.key.provider).bind(&flow.owner_replica).bind(flow.lease_expires_at).bind(flow.expires_at).bind(flow.created_at).bind(flow.updated_at).bind(state).execute(&self.pool).await.map_err(db_err)?; + if result.rows_affected() != 1 { + return Err(MetaError::Conflict("MCP flow creation rejected".into())); + } + Ok(()) + } + async fn consume_mcp_oauth_flow( + &self, + id: uuid::Uuid, + key: &engram_core::types::oauth::OAuthCredentialKey, + ) -> Result { + if key.subject_kind != engram_core::types::oauth::OAuthSubjectKind::Mcp { + return Err(MetaError::Conflict("MCP state unavailable".into())); + } + let mut tx = self.pool.begin().await.map_err(db_err)?; + let state: Option = sqlx::query_scalar("SELECT mcp_state FROM oauth_flows WHERE id=$1 AND subject_kind='mcp' AND subject_id=$2 AND provider=$3 AND status='pending' AND expires_at>$4 AND mcp_state IS NOT NULL FOR UPDATE") + .bind(id).bind(&key.subject_id).bind(&key.provider).bind(self.clock.now_utc()).fetch_optional(&mut *tx).await.map_err(db_err)?; + let state = state.ok_or_else(|| MetaError::Conflict("MCP state unavailable".into()))?; + sqlx::query("UPDATE oauth_flows SET mcp_state=NULL WHERE id=$1") + .bind(id) + .execute(&mut *tx) + .await + .map_err(db_err)?; + tx.commit().await.map_err(db_err)?; + serde_json::from_value(state).map_err(|e| MetaError::Serialization(e.to_string())) + } + async fn create_oauth_flow( &self, flow: engram_core::types::oauth::OAuthFlow, @@ -2559,6 +2669,8 @@ impl MetadataStore for PostgresStore { now: chrono::DateTime, delete_before: chrono::DateTime, ) -> Result { + sqlx::query("UPDATE oauth_flows SET mcp_state=NULL WHERE mcp_state IS NOT NULL AND (status<>'pending' OR expires_at<=$1 OR lease_expires_at<=$1)") + .bind(now).execute(&self.pool).await.map_err(db_err)?; let mut tx = self.pool.begin().await.map_err(db_err)?; let changed = sqlx::query( r#" diff --git a/crates/engram-sim/src/meta/mod.rs b/crates/engram-sim/src/meta/mod.rs index 48721d86e..018ccaf8c 100644 --- a/crates/engram-sim/src/meta/mod.rs +++ b/crates/engram-sim/src/meta/mod.rs @@ -243,6 +243,14 @@ pub struct SimDb { /// state, not part of the domain credential type. pub oauth_refresh_claims: std::collections::BTreeMap>, + pub mcp_refresh_claims: std::collections::BTreeMap< + engram_core::types::oauth::OAuthCredentialKey, + engram_core::types::mcp::McpRefreshClaim, + >, + pub mcp_oauth_admission: + std::collections::BTreeMap, + pub mcp_flow_states: + std::collections::BTreeMap, pub oauth_flows: std::collections::BTreeMap, pub session_oauth_bindings: std::collections::BTreeMap, diff --git a/crates/engram-sim/src/meta/store_impl.rs b/crates/engram-sim/src/meta/store_impl.rs index e98ff696e..04b4b48b2 100644 --- a/crates/engram-sim/src/meta/store_impl.rs +++ b/crates/engram-sim/src/meta/store_impl.rs @@ -5795,6 +5795,157 @@ impl MetadataStore for SimMetadataStore { Ok(row.clone()) } + async fn claim_mcp_refresh( + &self, + claim: &engram_core::types::mcp::McpRefreshClaim, + ) -> Result { + self.gate()?; + let now = self.now(); + if claim.key.subject_kind != engram_core::types::oauth::OAuthSubjectKind::Mcp + || claim.expires_at <= now + || claim.expires_at > now + chrono::Duration::seconds(30) + { + return Err(MetaError::Conflict("invalid refresh claim".into())); + } + let mut db = self.db.lock(); + let Some(row) = db.oauth_credentials.get_mut(&claim.key) else { + return Ok(false); + }; + if row.version != claim.version || row.revoked_at.is_some() || row.broken_at.is_some() { + return Ok(false); + } + row.broken_at = Some(now); + row.broken_reason = Some("refresh_pending".into()); + db.mcp_refresh_claims + .insert(claim.key.clone(), claim.clone()); + Ok(true) + } + async fn complete_mcp_refresh( + &self, + credential: engram_core::types::oauth::NewSealedOAuthCredential, + claim: &engram_core::types::mcp::McpRefreshClaim, + ) -> Result { + self.gate()?; + let now = self.now(); + let mut db = self.db.lock(); + if credential.key != claim.key + || db.mcp_refresh_claims.get(&claim.key) != Some(claim) + || claim.expires_at <= now + { + return Err(MetaError::Conflict("refresh claim lost".into())); + } + let row = db + .oauth_credentials + .get_mut(&claim.key) + .ok_or(MetaError::NotFound)?; + if row.version != claim.version + || row.revoked_at.is_some() + || row.broken_reason.as_deref() != Some("refresh_pending") + { + return Err(MetaError::Conflict("refresh claim lost".into())); + } + row.wrapped_dek = credential.wrapped_dek; + row.nonce = credential.nonce; + row.ciphertext = credential.ciphertext; + row.key_id = credential.key_id; + row.metadata = credential.metadata; + row.expires_at = credential.expires_at; + row.version += 1; + row.updated_at = now; + row.broken_at = None; + row.broken_reason = None; + let result = row.clone(); + db.mcp_refresh_claims.remove(&claim.key); + db.oauth_refresh_claims.remove(&claim.key); + Ok(result) + } + async fn acquire_mcp_oauth_admission( + &self, + lease: &engram_core::types::mcp::McpOAuthAdmission, + ) -> Result { + self.gate()?; + let now = self.now(); + if lease.deployment_id.is_empty() + || lease.connection_id.is_empty() + || lease.expires_at <= now + || lease.expires_at > now + chrono::Duration::seconds(30) + { + return Err(MetaError::Conflict("invalid OAuth admission".into())); + } + let mut db = self.db.lock(); + db.mcp_oauth_admission.retain(|_, l| l.expires_at > now); + if db.mcp_oauth_admission.contains_key(&lease.id) + || db.mcp_oauth_admission.values().any(|l| { + l.deployment_id == lease.deployment_id && l.connection_id == lease.connection_id + }) + || db + .mcp_oauth_admission + .values() + .filter(|l| l.deployment_id == lease.deployment_id) + .count() + >= 4 + { + return Ok(false); + } + db.mcp_oauth_admission.insert(lease.id, lease.clone()); + Ok(true) + } + async fn release_mcp_oauth_admission( + &self, + lease: &engram_core::types::mcp::McpOAuthAdmission, + ) -> Result<(), MetaError> { + self.gate()?; + let mut db = self.db.lock(); + if db.mcp_oauth_admission.get(&lease.id) == Some(lease) { + db.mcp_oauth_admission.remove(&lease.id); + } + Ok(()) + } + + async fn create_mcp_oauth_flow( + &self, + flow: engram_core::types::oauth::OAuthFlow, + state: engram_core::types::mcp::McpSealedState, + ) -> Result<(), MetaError> { + use engram_core::types::oauth::{OAuthFlowStatus, OAuthSubjectKind}; + self.gate()?; + let mut db = self.db.lock(); + if flow.key.subject_kind != OAuthSubjectKind::Mcp + || flow.status != OAuthFlowStatus::Pending + || flow.expires_at <= self.now() + || db.oauth_flows.contains_key(&flow.id) + || db + .oauth_flows + .values() + .any(|f| f.key == flow.key && f.status == OAuthFlowStatus::Pending) + { + return Err(MetaError::Conflict("MCP flow creation rejected".into())); + } + db.mcp_flow_states.insert(flow.id, state); + db.oauth_flows.insert(flow.id, flow); + Ok(()) + } + async fn consume_mcp_oauth_flow( + &self, + id: uuid::Uuid, + key: &engram_core::types::oauth::OAuthCredentialKey, + ) -> Result { + use engram_core::types::oauth::{OAuthFlowStatus, OAuthSubjectKind}; + self.gate()?; + let mut db = self.db.lock(); + let flow = db.oauth_flows.get(&id).ok_or(MetaError::NotFound)?; + if key.subject_kind != OAuthSubjectKind::Mcp + || &flow.key != key + || flow.status != OAuthFlowStatus::Pending + || flow.expires_at <= self.now() + { + return Err(MetaError::Conflict("MCP state unavailable".into())); + } + db.mcp_flow_states + .remove(&id) + .ok_or_else(|| MetaError::Conflict("MCP state consumed".into())) + } + async fn create_oauth_flow( &self, flow: engram_core::types::oauth::OAuthFlow, @@ -5951,6 +6102,13 @@ impl MetadataStore for SimMetadataStore { let before = db.oauth_flows.len(); db.oauth_flows .retain(|_, flow| !flow.status.is_terminal() || flow.updated_at >= delete_before); + let pending: std::collections::BTreeSet<_> = db + .oauth_flows + .values() + .filter(|f| !f.status.is_terminal()) + .map(|f| f.id) + .collect(); + db.mcp_flow_states.retain(|id, _| pending.contains(id)); Ok(changed + (before - db.oauth_flows.len()) as u64) } } diff --git a/crates/engram-sim/tests/meta_conformance.rs b/crates/engram-sim/tests/meta_conformance.rs index 0d490d898..400a85172 100644 --- a/crates/engram-sim/tests/meta_conformance.rs +++ b/crates/engram-sim/tests/meta_conformance.rs @@ -7581,3 +7581,242 @@ conformance!( t_mcp_guest_binding_rejected, super::mcp_guest_binding_rejected ); + +// Consuming a redirect must remove its encrypted state before any exchange. +async fn mcp_flow_consume(ctx: &Ctx) { + use engram_core::types::{mcp::*, oauth::*}; + let now = ctx.clock.now_utc(); + let key = OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: "connection-a".into(), + provider: "registered-a".into(), + }; + let id = uuid::Uuid::from_u128(810); + let flow = OAuthFlow { + id, + key: key.clone(), + owner_replica: "replica-a".into(), + lease_expires_at: now + chrono::Duration::minutes(10), + expires_at: now + chrono::Duration::minutes(10), + status: OAuthFlowStatus::Pending, + error_code: None, + created_at: now, + updated_at: now, + }; + let state = McpSealedState { + wrapped_dek: vec![1; 32], + nonce: vec![2; 12], + ciphertext: vec![3; 64], + key_id: "test".into(), + }; + ctx.meta + .create_mcp_oauth_flow(flow.clone(), state.clone()) + .await + .unwrap(); + let mut wrong = key.clone(); + wrong.subject_id = "connection-b".into(); + assert!(ctx.meta.consume_mcp_oauth_flow(id, &wrong).await.is_err()); + let (a, b) = tokio::join!( + ctx.meta.consume_mcp_oauth_flow(id, &key), + ctx.meta.consume_mcp_oauth_flow(id, &key) + ); + assert_eq!(usize::from(a.is_ok()) + usize::from(b.is_ok()), 1); + assert_eq!(a.or(b).unwrap(), state); + assert!(ctx.meta.consume_mcp_oauth_flow(id, &key).await.is_err()); + ctx.meta + .finish_oauth_flow_unowned(id, OAuthFlowStatus::Succeeded, None) + .await + .unwrap(); + let mut expired = flow; + expired.id = uuid::Uuid::from_u128(811); + ctx.meta + .create_mcp_oauth_flow(expired, state) + .await + .unwrap(); + ctx.clock.advance(Duration::from_secs(600)); + assert!(ctx + .meta + .consume_mcp_oauth_flow(uuid::Uuid::from_u128(811), &key) + .await + .is_err()); +} +conformance!(t_mcp_flow_consume, mcp_flow_consume); + +async fn mcp_admission_and_refresh(ctx: &Ctx) { + use engram_core::types::{mcp::*, oauth::*}; + let now = ctx.clock.now_utc(); + let key = OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: "connection-a".into(), + provider: "registered-a".into(), + }; + let candidate = NewSealedOAuthCredential { + key: key.clone(), + wrapped_dek: vec![1; 32], + nonce: vec![2; 12], + ciphertext: vec![3; 64], + key_id: "test".into(), + metadata: OAuthAccountMetadata::default(), + expires_at: Some(now), + }; + let row = ctx + .meta + .put_oauth_credential(candidate.clone(), None) + .await + .unwrap(); + let claim = McpRefreshClaim { + id: uuid::Uuid::from_u128(901), + key: key.clone(), + version: row.version, + expires_at: now + chrono::Duration::seconds(20), + }; + let (a, b) = tokio::join!( + ctx.meta.claim_mcp_refresh(&claim), + ctx.meta.claim_mcp_refresh(&claim) + ); + assert_eq!(usize::from(a.unwrap()) + usize::from(b.unwrap()), 1); + assert!(ctx + .meta + .get_oauth_credential(&key) + .await + .unwrap() + .unwrap() + .broken_at + .is_some()); + let mut wrong = claim.clone(); + wrong.id = uuid::Uuid::from_u128(902); + assert!(ctx + .meta + .complete_mcp_refresh(candidate.clone(), &wrong) + .await + .is_err()); + let refreshed = ctx + .meta + .complete_mcp_refresh(candidate.clone(), &claim) + .await + .unwrap(); + assert_eq!(refreshed.version, row.version + 1); + assert!(refreshed.broken_at.is_none()); + let mut uncertain = claim; + uncertain.version = refreshed.version; + assert!(ctx.meta.claim_mcp_refresh(&uncertain).await.unwrap()); + ctx.clock.advance(Duration::from_secs(21)); + assert!(ctx + .meta + .complete_mcp_refresh(candidate, &uncertain) + .await + .is_err()); + uncertain.expires_at = ctx.clock.now_utc() + chrono::Duration::seconds(20); + assert!( + !ctx.meta.claim_mcp_refresh(&uncertain).await.unwrap(), + "uncertain rotation must require reconnect" + ); + let lease = |n| McpOAuthAdmission { + id: uuid::Uuid::from_u128(n), + deployment_id: "deployment-a".into(), + connection_id: format!("connection-{n}"), + expires_at: ctx.clock.now_utc() + chrono::Duration::seconds(15), + }; + let leases: Vec<_> = (1..=5).map(lease).collect(); + for item in &leases[..4] { + assert!(ctx.meta.acquire_mcp_oauth_admission(item).await.unwrap()); + } + assert!(!ctx + .meta + .acquire_mcp_oauth_admission(&leases[4]) + .await + .unwrap()); + ctx.meta + .release_mcp_oauth_admission(&leases[0]) + .await + .unwrap(); + assert!(ctx + .meta + .acquire_mcp_oauth_admission(&leases[4]) + .await + .unwrap()); + let mut same_connection = leases[4].clone(); + same_connection.id = uuid::Uuid::from_u128(99); + assert!(!ctx + .meta + .acquire_mcp_oauth_admission(&same_connection) + .await + .unwrap()); + ctx.clock.advance(Duration::from_secs(15)); + same_connection.expires_at = ctx.clock.now_utc() + chrono::Duration::seconds(15); + assert!(ctx + .meta + .acquire_mcp_oauth_admission(&same_connection) + .await + .unwrap()); + let contenders: Vec<_> = (201..=205) + .map(|n| McpOAuthAdmission { + id: uuid::Uuid::from_u128(n), + deployment_id: "deployment-race".into(), + connection_id: format!("connection-{n}"), + expires_at: ctx.clock.now_utc() + chrono::Duration::seconds(15), + }) + .collect(); + let (a, b, c, d, e) = tokio::join!( + ctx.meta.acquire_mcp_oauth_admission(&contenders[0]), + ctx.meta.acquire_mcp_oauth_admission(&contenders[1]), + ctx.meta.acquire_mcp_oauth_admission(&contenders[2]), + ctx.meta.acquire_mcp_oauth_admission(&contenders[3]), + ctx.meta.acquire_mcp_oauth_admission(&contenders[4]), + ); + assert_eq!( + [a, b, c, d, e] + .into_iter() + .filter(|r| *r.as_ref().unwrap()) + .count(), + 4 + ); + // Revocation fences both a dispatched refresh and a late reconnect CAS. + let mut replacement = NewSealedOAuthCredential { + key: key.clone(), + wrapped_dek: vec![1; 32], + nonce: vec![2; 12], + ciphertext: vec![3; 64], + key_id: "test".into(), + metadata: OAuthAccountMetadata::default(), + expires_at: None, + }; + replacement.key.subject_id = "connection-revoked".into(); + let row = ctx + .meta + .put_oauth_credential(replacement.clone(), None) + .await + .unwrap(); + let claim = McpRefreshClaim { + id: uuid::Uuid::from_u128(903), + key: replacement.key.clone(), + version: row.version, + expires_at: ctx.clock.now_utc() + chrono::Duration::seconds(20), + }; + assert!(ctx.meta.claim_mcp_refresh(&claim).await.unwrap()); + let receipt = ctx + .meta + .revoke_oauth_credential(&replacement.key, row.version) + .await + .unwrap(); + assert_eq!(receipt.version, row.version + 1); + assert!(ctx + .meta + .complete_mcp_refresh(replacement.clone(), &claim) + .await + .is_err()); + assert!(ctx + .meta + .put_oauth_credential(replacement.clone(), Some(row.version)) + .await + .is_err()); + assert!(ctx + .meta + .get_oauth_credential(&replacement.key) + .await + .unwrap() + .unwrap() + .revoked_at + .is_some()); +} +conformance!(t_mcp_admission_and_refresh, mcp_admission_and_refresh); diff --git a/deploy/migrations/0127_mcp_oauth_authority.sql b/deploy/migrations/0127_mcp_oauth_authority.sql new file mode 100644 index 000000000..91d44647e --- /dev/null +++ b/deploy/migrations/0127_mcp_oauth_authority.sql @@ -0,0 +1,14 @@ +-- MCP encrypted redirect state stays with the existing flow authority. +ALTER TABLE oauth_flows ADD COLUMN mcp_state jsonb; +ALTER TABLE oauth_flows ADD CONSTRAINT mcp_state_subject CHECK (mcp_state IS NULL OR subject_kind='mcp'); +ALTER TABLE oauth_credentials ADD COLUMN mcp_refresh_claim uuid; +CREATE TABLE mcp_oauth_admission ( + deployment_id text NOT NULL, + slot smallint NOT NULL CHECK (slot BETWEEN 0 AND 3), + id uuid NOT NULL UNIQUE, + connection_id text NOT NULL, + expires_at timestamptz NOT NULL, + PRIMARY KEY (deployment_id, slot), + UNIQUE (deployment_id, connection_id) +); +CREATE INDEX mcp_oauth_admission_expiry ON mcp_oauth_admission (expires_at); diff --git a/docs/adr/0125-isolated-external-mcp.md b/docs/adr/0125-isolated-external-mcp.md index 88bbda41d..ba231f55d 100644 --- a/docs/adr/0125-isolated-external-mcp.md +++ b/docs/adr/0125-isolated-external-mcp.md @@ -35,6 +35,19 @@ OAuth have separate budgets. The worker cannot authorize a tool call or refresh credential on its own. The coordinator is the only credential and refresh authority. The worker receives secrets only after admission, near dispatch. +The coordinator exposes credential control only on a separate, default-off mTLS +listener. Exact client certificate pins identify the trusted orchestrator role. +The existing cleartext application gRPC listener never serves this service. The +orchestrator may hold a short-lived credential lease in memory for one admitted +worker operation. It must not queue, cache, log, persist, or return the value to a +guest or browser. Resource calls use the orchestrator worker role; OAuth metadata +and token calls use the coordinator worker role. A lease checks the actual sealed +identity, config revision, and authorization generation, including when publication +in the orchestrator database failed. Worker boot and operator policy must match +the frozen operation; a new identity lookup cannot replace a frozen boot identity. +Worker boot binds an operation, not durable credential authority. After a worker +restart, fresh operations can use the current boot with the same sealed authority. + ### Reviewed catalogs and session binding Reuse named integration connections and profile integration grants. Discovery @@ -102,6 +115,19 @@ Use single-flight refresh and rotating-token compare-and-swap. Do not fall back to stale credentials or expand scope silently. Send access tokens only to the resource; send client secrets and refresh tokens only to the token endpoint. +MCP redirect state is sealed in the existing OAuth flow store and consumed +atomically before token exchange. Coordinator OAuth requests use durable admission +with four concurrent slots per deployment and one per connection. Refresh claims +are exclusive and version-fenced; a token deadline ends before its claim. An +uncertain rotating exchange leaves the credential unavailable until reconnect. +No expired-claim retry may reuse the previous refresh token. Ordinary refresh +preserves authority identity; reconnect and API-key rotation create a new identity +bound to the next expected authorization generation. Before switching credential +kinds, references, or registrations, the orchestrator serializes the connection +transition and revokes the old authority with its exact identity and version. It +verifies the durable revocation receipt before acquisition and publication of the +replacement. Failed publication cannot restore the old authority. + ### Authorization and irreversible dispatch Before execution, validate the active session/run, immutable binding, principal, From 9bd9524927da85e5681ac279b9e193a04199a8d6 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Sun, 1 Nov 2026 23:53:09 -0500 Subject: [PATCH 011/279] Add coordinator MCP authority and dedicated mTLS control --- Cargo.lock | 1 + buf.gen.yaml | 15 +- crates/engram-coordinator/Cargo.toml | 2 +- crates/engram-coordinator/src/config.rs | 5 +- .../engram-coordinator/src/grpc_app/oauth.rs | 23 +- crates/engram-coordinator/src/lib.rs | 20 +- crates/engram-coordinator/src/main.rs | 5 + crates/engram-coordinator/src/mcp/control.rs | 395 +++++++++ crates/engram-coordinator/src/mcp/io.rs | 125 +++ .../engram-coordinator/src/mcp/lifecycle.rs | 116 +++ crates/engram-coordinator/src/mcp/mod.rs | 365 +++++++++ crates/engram-coordinator/src/mcp/model.rs | 98 +++ crates/engram-coordinator/src/mcp/oauth.rs | 586 ++++++++++++++ crates/engram-coordinator/src/mcp/startup.rs | 103 +++ crates/engram-coordinator/src/mcp/worker.rs | 275 +++++++ crates/engram-coordinator/src/oauth.rs | 21 + .../engram-coordinator/src/oauth_refresh.rs | 5 + .../tests/fixtures/mcp-worker.mjs | 107 +++ .../engram-coordinator/tests/mcp_authority.rs | 276 +++++++ .../engram-coordinator/tests/mcp_control.rs | 171 ++++ .../tests/mcp_credentials.rs | 26 + crates/engram-coordinator/tests/mcp_oauth.rs | 598 ++++++++++++++ crates/engram-crypto/src/lib.rs | 9 + crates/engram-protocol/build.rs | 10 + .../proto/engram/mcp/v1/control.proto | 144 ++++ crates/engram-protocol/src/lib.rs | 2 + crates/engram-protocol/src/mcp.rs | 14 + deploy/mcp-control.example.json | 65 ++ .../src/gen/engram/mcp/v1/control_pb.ts | 751 ++++++++++++++++++ .../control-CredentialControl_connectquery.ts | 35 + web/src/gen/engram/mcp/v1/control_pb.ts | 751 ++++++++++++++++++ workspace-hack/Cargo.toml | 2 +- 32 files changed, 5104 insertions(+), 17 deletions(-) create mode 100644 crates/engram-coordinator/src/mcp/control.rs create mode 100644 crates/engram-coordinator/src/mcp/io.rs create mode 100644 crates/engram-coordinator/src/mcp/lifecycle.rs create mode 100644 crates/engram-coordinator/src/mcp/mod.rs create mode 100644 crates/engram-coordinator/src/mcp/model.rs create mode 100644 crates/engram-coordinator/src/mcp/oauth.rs create mode 100644 crates/engram-coordinator/src/mcp/startup.rs create mode 100644 crates/engram-coordinator/src/mcp/worker.rs create mode 100644 crates/engram-coordinator/tests/fixtures/mcp-worker.mjs create mode 100644 crates/engram-coordinator/tests/mcp_authority.rs create mode 100644 crates/engram-coordinator/tests/mcp_control.rs create mode 100644 crates/engram-coordinator/tests/mcp_oauth.rs create mode 100644 crates/engram-protocol/proto/engram/mcp/v1/control.proto create mode 100644 crates/engram-protocol/src/mcp.rs create mode 100644 deploy/mcp-control.example.json create mode 100644 orchestrator/src/gen/engram/mcp/v1/control_pb.ts create mode 100644 web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts create mode 100644 web/src/gen/engram/mcp/v1/control_pb.ts diff --git a/Cargo.lock b/Cargo.lock index 766124216..6b18db503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8078,6 +8078,7 @@ dependencies = [ "socket2", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-stream", "tokio-util", "tower", diff --git a/buf.gen.yaml b/buf.gen.yaml index ce7b49048..0fddb637d 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -1,12 +1,6 @@ -# TS codegen for the ADR 0051 app contract (`just gen-proto`). -# -# Only `engram/app/**` is generated for TS — host_service.proto is the -# coord↔host transport (tonic via crates/engram-protocol/build.rs) and -# never reaches the web/orchestrator tier. The `paths` filter values are -# cwd-relative and must include the input-directory prefix (buf v2 -# semantics). The app dir lands with Tasks 4-5; until then `buf -# generate` produces no output (the CI drift check guards on the dir -# existing). +# TS codegen for the application and dedicated internal MCP control contracts. +# host_service.proto remains coordinator-to-host transport, generated by tonic. +# Input paths are cwd-relative and include the input-directory prefix (buf v2). version: v2 # Wipe the `out` dirs before generating so deleted/renamed protos don't # leave stale bindings behind for the drift check to miss. @@ -14,7 +8,8 @@ clean: true inputs: - directory: crates/engram-protocol/proto paths: - - crates/engram-protocol/proto/engram/app # TS needs only the app contract + - crates/engram-protocol/proto/engram/app + - crates/engram-protocol/proto/engram/mcp # Dedicated internal mTLS control # Asymmetry is deliberate: query-es (TanStack Query hooks) is web-only — # the orchestrator is a server and needs plain messages/clients only. plugins: diff --git a/crates/engram-coordinator/Cargo.toml b/crates/engram-coordinator/Cargo.toml index 56459b615..5b365c164 100644 --- a/crates/engram-coordinator/Cargo.toml +++ b/crates/engram-coordinator/Cargo.toml @@ -68,7 +68,7 @@ sqlx.workspace = true reqwest.workspace = true tokio.workspace = true tokio-stream.workspace = true -tonic.workspace = true +tonic = { workspace = true, features = ["tls-ring"] } tonic-reflection.workspace = true tower.workspace = true tower-http.workspace = true diff --git a/crates/engram-coordinator/src/config.rs b/crates/engram-coordinator/src/config.rs index e1361a40c..a92ae6b27 100644 --- a/crates/engram-coordinator/src/config.rs +++ b/crates/engram-coordinator/src/config.rs @@ -24,13 +24,15 @@ pub struct CoordinatorConfig { // `engram-auth` AuthConfig) is removed along with the whole `engram-auth` // crate. The coordinator no longer resolves human identity — `auth_tokens` // (the deployment bearer) and `app_grpc_tokens` (the app-gRPC bearer) are - // the only machine-caller credentials it knows about. + // the generic machine-caller credentials. MCP uses separate mTLS pins. /// Local address the harness-channel TCP listener binds to. /// Harnesses spawned via `start_agent` on the Process backend dial /// this from the same host. `127.0.0.1:0` (default) lets the OS /// pick a free port; the coordinator reads back the bound address /// and plumbs it into the agent's env at session-create time. pub harness_listen_addr: std::net::SocketAddr, + /// Optional operator file for the separate MCP mTLS listener. Unset disables it. + pub mcp_control_config: Option, /// Address the orchestrator-facing app gRPC server binds to /// (ADR 0051 §2.3). Serves Session/ShellRelay/Fleet/Image/Secret /// beside the axum API. Loopback by default — the orchestrator is @@ -77,6 +79,7 @@ impl Default for CoordinatorConfig { harness_listen_addr: "127.0.0.1:0" .parse() .expect("default harness_listen_addr must parse"), + mcp_control_config: None, app_grpc_addr: "127.0.0.1:50061" .parse() .expect("default app_grpc_addr must parse"), diff --git a/crates/engram-coordinator/src/grpc_app/oauth.rs b/crates/engram-coordinator/src/grpc_app/oauth.rs index f980d26a6..594b3e00f 100644 --- a/crates/engram-coordinator/src/grpc_app/oauth.rs +++ b/crates/engram-coordinator/src/grpc_app/oauth.rs @@ -33,7 +33,9 @@ fn subject_from_proto( let kind = match app::OauthSubjectKind::try_from(subject.kind) { Ok(app::OauthSubjectKind::User) => OAuthSubjectKind::User, Ok(app::OauthSubjectKind::Connector) => OAuthSubjectKind::Connector, - Ok(app::OauthSubjectKind::Mcp) => OAuthSubjectKind::Mcp, + Ok(app::OauthSubjectKind::Mcp) => { + return Err("MCP credentials require the dedicated control service") + } Ok(app::OauthSubjectKind::UserConnector) => OAuthSubjectKind::UserConnector, _ => return Err("subject kind is required"), }; @@ -391,7 +393,11 @@ impl app::o_auth_credential_service_server::OAuthCredentialService for AppOAuthC let kind = match app::OauthSubjectKind::try_from(subject.kind) { Ok(app::OauthSubjectKind::User) => OAuthSubjectKind::User, Ok(app::OauthSubjectKind::Connector) => OAuthSubjectKind::Connector, - Ok(app::OauthSubjectKind::Mcp) => OAuthSubjectKind::Mcp, + Ok(app::OauthSubjectKind::Mcp) => { + return Err(Status::permission_denied( + "MCP credentials require the dedicated control service", + )) + } Ok(app::OauthSubjectKind::UserConnector) => OAuthSubjectKind::UserConnector, _ => return Err(Status::invalid_argument("subject kind is required")), }; @@ -552,3 +558,16 @@ impl app::o_auth_credential_service_server::OAuthCredentialService for AppOAuthC })) } } + +#[cfg(test)] +mod mcp_boundary_tests { + use super::*; + #[test] + fn mcp_subject_rejects_all_generic_app_oauth_operations() { + assert!(subject_from_proto(Some(app::OAuthSubject { + kind: app::OauthSubjectKind::Mcp as i32, + id: "connection-a".into() + })) + .is_err()); + } +} diff --git a/crates/engram-coordinator/src/lib.rs b/crates/engram-coordinator/src/lib.rs index 90b10442f..ec3e97046 100644 --- a/crates/engram-coordinator/src/lib.rs +++ b/crates/engram-coordinator/src/lib.rs @@ -31,6 +31,7 @@ pub mod idle_detector; pub mod idle_evictor; pub mod integration_ops; pub mod integrations; +pub mod mcp; pub mod metrics; pub mod oauth; pub mod oauth_client_credentials; @@ -376,6 +377,19 @@ pub async fn run_with_registry_and_local( state.services.host.set_upload_sink(sink); } + // The optional credential service has its own authenticated listener. + let mcp_control = mcp::startup::start( + cfg.mcp_control_config.as_deref(), + state.oauth.clone(), + state.subscribe_shutdown(), + ) + .await + .map_err(|_| { + CoordinatorError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "invalid MCP control configuration", + )) + })?; // ADR 0051 B: the orchestrator-facing app gRPC surface, served // BESIDE the axum API (additive — no REST route changes). Spawned so // a runtime serve crash doesn't take the web surface down with it @@ -446,10 +460,12 @@ pub async fn run_with_registry_and_local( .await .map_err(CoordinatorError::Io)?; - // Graceful shutdown drains both servers: axum has returned, now - // wait for the app gRPC serve loop to finish its own drain. A + // Axum has drained. Wait for both control listeners to finish. A // JoinError here means the spawned task panicked — log it rather // than turning a clean web-side shutdown into a process error. + if let Some(task) = mcp_control { + let _ = task.await; + } if let Err(e) = app_grpc.await { tracing::error!(error = %e, "app gRPC server task panicked"); } diff --git a/crates/engram-coordinator/src/main.rs b/crates/engram-coordinator/src/main.rs index 3639cba93..172563e96 100644 --- a/crates/engram-coordinator/src/main.rs +++ b/crates/engram-coordinator/src/main.rs @@ -77,6 +77,10 @@ struct Cli { #[arg(long, env = "ENGRAM_APP_GRPC_ADDR", default_value = "127.0.0.1:50061")] app_grpc_addr: std::net::SocketAddr, + /// Operator configuration file for the separate MCP credential control listener. + #[arg(long, env = "ENGRAM_MCP_CONTROL_CONFIG")] + mcp_control_config: Option, + /// Comma-separated bearer tokens accepted on the app gRPC surface /// (ADR 0051 §5) — the orchestrator's machine credential, separate /// from `--auth-tokens` (different caller, different blast radius, @@ -368,6 +372,7 @@ async fn main() -> Result<(), CoordinatorError> { .collect(), harness_listen_addr: cli.harness_listen_addr, app_grpc_addr: cli.app_grpc_addr, + mcp_control_config: cli.mcp_control_config.clone(), // Same empty-entry stripping as `auth_tokens` above (clap's // value_delimiter turns an unset env into one "" entry), plus // trim so `a, b` rotation lists don't mint a " b" token. An diff --git a/crates/engram-coordinator/src/mcp/control.rs b/crates/engram-coordinator/src/mcp/control.rs new file mode 100644 index 000000000..0ce4ef252 --- /dev/null +++ b/crates/engram-coordinator/src/mcp/control.rs @@ -0,0 +1,395 @@ +//! Dedicated mTLS service. Never add this router to the app gRPC listener. +use super::{model as domain, oauth::Callback, worker, McpAuthority, McpError}; +use engram_core::types::org_secret::{McpSecretReservation, McpSecretRole}; +use engram_protocol::mcp as wire; +use sha2::{Digest, Sha256}; +use std::{collections::BTreeSet, sync::Arc, time::Duration}; +use tonic::{Request, Response, Status}; +use wire::credential_control_server::{CredentialControl, CredentialControlServer}; + +type RpcResult = std::result::Result, Status>; +pub struct ControlTls { + pub certificate: Vec, + pub private_key: Vec, + pub client_ca: Vec, + pub orchestrator_fingerprints: Vec, +} +#[derive(Clone)] +struct Control { + authority: Arc, + pins: Arc>, + admission: Arc, +} +pub fn server( + authority: Arc, + tls: ControlTls, +) -> super::Result { + if tls.orchestrator_fingerprints.is_empty() + || tls.orchestrator_fingerprints.len() > 8 + || tls.orchestrator_fingerprints.iter().any(|s| { + s.len() != 95 + || s.split(':').any(|p| { + p.len() != 2 + || !p + .bytes() + .all(|b| b.is_ascii_digit() || (b'A'..=b'F').contains(&b)) + }) + }) + { + return Err(McpError::Invalid); + } + engram_tls::install_provider(); + let service = Control { + authority, + pins: Arc::new(tls.orchestrator_fingerprints.into_iter().collect()), + admission: Arc::new(tokio::sync::Semaphore::new(32)), + }; + Ok(tonic::transport::Server::builder() + .tls_config( + tonic::transport::ServerTlsConfig::new() + .identity(tonic::transport::Identity::from_pem( + tls.certificate, + tls.private_key, + )) + .client_ca_root(tonic::transport::Certificate::from_pem(tls.client_ca)), + ) + .map_err(|_| McpError::Invalid)? + .concurrency_limit_per_connection(16) + .timeout(Duration::from_secs(30)) + .max_concurrent_streams(16) + .add_service( + CredentialControlServer::new(service) + .max_decoding_message_size(32768) + .max_encoding_message_size(32768), + )) +} +impl Control { + fn check( + &self, + req: &Request, + protocol: u32, + ) -> std::result::Result { + let certs = req + .peer_certs() + .ok_or_else(|| Status::unauthenticated("client certificate required"))?; + let cert = certs + .first() + .ok_or_else(|| Status::unauthenticated("client certificate required"))?; + let pin = Sha256::digest(cert.as_ref()) + .iter() + .map(|b| format!("{b:02X}")) + .collect::>() + .join(":"); + if !self.pins.contains(&pin) { + return Err(Status::permission_denied("caller denied")); + } + if protocol != wire::PROTOCOL { + return Err(Status::invalid_argument("unsupported control protocol")); + } + self.admission + .clone() + .try_acquire_owned() + .map_err(|_| Status::resource_exhausted("control capacity exceeded")) + } +} +fn error(e: McpError) -> Status { + match e { + McpError::Invalid => Status::invalid_argument("invalid credential request"), + McpError::Denied => Status::permission_denied("credential request denied"), + McpError::Stale => Status::failed_precondition("credential context changed"), + _ => Status::unavailable("credential unavailable"), + } +} +fn required(value: Option) -> std::result::Result { + value.ok_or_else(|| Status::invalid_argument("required field missing")) +} +fn actor(value: &str) -> std::result::Result<(), Status> { + if value.is_empty() || value.len() > 256 { + Err(Status::invalid_argument("actor required")) + } else { + Ok(()) + } +} +fn destination( + value: Option, +) -> std::result::Result { + let v = required(value)?; + Ok(domain::Destination { + zone_id: v.zone_id, + endpoint: v.endpoint, + }) +} +fn change( + value: Option, +) -> std::result::Result { + let v = required(value)?; + Ok(domain::ChangeContext { + connection_id: v.connection_id, + config_revision: v.config_revision, + previous_authorization_generation: v.previous_authorization_generation, + resource: destination(v.resource)?, + }) +} +fn context( + value: Option, +) -> std::result::Result { + let v = required(value)?; + Ok(domain::AuthorityContext { + connection_id: v.connection_id, + config_revision: v.config_revision, + authorization_generation: v.authorization_generation, + credential_identity: v.credential_identity, + resource: destination(v.resource)?, + policy_digest: v.policy_digest, + policy_revision: v.policy_revision, + }) +} +fn reference( + value: wire::CredentialReference, +) -> std::result::Result { + match wire::CredentialKind::try_from(value.kind) + .map_err(|_| Status::invalid_argument("invalid credential kind"))? + { + wire::CredentialKind::ApiKey | wire::CredentialKind::ClientSecret + if value.registration_id.is_empty() + && !value.name.is_empty() + && !value.connection_id.is_empty() + && value.generation >= 1 => + { + let role = if value.kind == wire::CredentialKind::ApiKey as i32 { + McpSecretRole::ApiKey + } else { + McpSecretRole::ClientSecret + }; + let reservation = McpSecretReservation { + name: value.name, + connection_id: value.connection_id, + role, + generation: value.generation, + }; + Ok(if role == McpSecretRole::ApiKey { + domain::CredentialReference::ApiKey { reservation } + } else { + domain::CredentialReference::ClientSecret { reservation } + }) + } + wire::CredentialKind::Oauth + if !value.registration_id.is_empty() + && value.name.is_empty() + && value.connection_id.is_empty() + && value.generation == 0 => + { + Ok(domain::CredentialReference::OAuth { + registration_id: value.registration_id, + }) + } + _ => Err(Status::invalid_argument("invalid credential reference")), + } +} +fn context_wire(v: domain::AuthorityContext) -> wire::AuthorityContext { + wire::AuthorityContext { + connection_id: v.connection_id, + config_revision: v.config_revision, + authorization_generation: v.authorization_generation, + credential_identity: v.credential_identity, + resource: Some(wire::Destination { + zone_id: v.resource.zone_id, + endpoint: v.resource.endpoint, + }), + policy_digest: v.policy_digest, + policy_revision: v.policy_revision, + } +} +fn reference_wire(v: domain::CredentialReference) -> wire::CredentialReference { + match v { + domain::CredentialReference::ApiKey { reservation } + | domain::CredentialReference::ClientSecret { reservation } => wire::CredentialReference { + kind: if reservation.role == McpSecretRole::ApiKey { + wire::CredentialKind::ApiKey + } else { + wire::CredentialKind::ClientSecret + } as i32, + name: reservation.name, + connection_id: reservation.connection_id, + generation: reservation.generation, + registration_id: String::new(), + }, + domain::CredentialReference::OAuth { registration_id } => wire::CredentialReference { + kind: wire::CredentialKind::Oauth as i32, + registration_id, + ..Default::default() + }, + } +} +fn status_wire(v: domain::CredentialStatus) -> wire::CredentialStatus { + wire::CredentialStatus { + protocol: wire::PROTOCOL, + context: Some(context_wire(v.context)), + reference: Some(reference_wire(v.reference)), + version: v.version, + status: v.status, + } +} +fn status(v: wire::CredentialStatus) -> std::result::Result { + if v.protocol != wire::PROTOCOL { + return Err(Status::invalid_argument("unsupported control protocol")); + } + Ok(domain::CredentialStatus { + context: context(v.context)?, + reference: reference(required(v.reference)?)?, + version: v.version, + status: v.status, + }) +} +#[tonic::async_trait] +impl CredentialControl for Control { + async fn put_secret( + &self, + req: Request, + ) -> RpcResult { + let _permit = self.check(&req, req.get_ref().protocol)?; + let v = req.into_inner(); + actor(&v.actor_id)?; + let role = match wire::CredentialKind::try_from(v.kind) { + Ok(wire::CredentialKind::ApiKey) => McpSecretRole::ApiKey, + Ok(wire::CredentialKind::ClientSecret) => McpSecretRole::ClientSecret, + _ => return Err(Status::invalid_argument("invalid secret kind")), + }; + let expected = v.expected.map(reference).transpose()?; + Ok(Response::new(status_wire( + self.authority + .put_secret( + &change(v.context)?, + role, + expected.as_ref(), + &v.header, + &v.value, + ) + .await + .map_err(error)?, + ))) + } + async fn begin_authorization( + &self, + req: Request, + ) -> RpcResult { + let _permit = self.check(&req, req.get_ref().protocol)?; + let v = req.into_inner(); + actor(&v.actor_id)?; + let secret = v.client_secret.map(reference).transpose()?; + let b = self + .authority + .begin( + &change(v.context)?, + &v.registration_id, + secret.as_ref(), + v.expected_version, + &v.actor_id, + v.sharing_consent, + ) + .await + .map_err(error)?; + Ok(Response::new(wire::BeginAuthorizationResponse { + protocol: wire::PROTOCOL, + flow_id: b.flow_id.to_string(), + context: Some(context_wire(b.context)), + authorization_url: b.authorization_url, + expires_at: b.expires_at.timestamp_millis(), + })) + } + async fn complete_authorization( + &self, + req: Request, + ) -> RpcResult { + let _permit = self.check(&req, req.get_ref().protocol)?; + let v = req.into_inner(); + actor(&v.actor_id)?; + let callback = Callback { + context: context(v.context)?, + registration_id: v.registration_id, + flow_id: v + .flow_id + .parse() + .map_err(|_| Status::invalid_argument("invalid flow id"))?, + state: v.state, + redirect_uri: v.redirect_uri, + issuer: v.issuer, + code: v.code, + actor_id: v.actor_id, + }; + Ok(Response::new(status_wire( + self.authority.callback(&callback).await.map_err(error)?, + ))) + } + async fn get_status( + &self, + req: Request, + ) -> RpcResult { + let _permit = self.check(&req, req.get_ref().protocol)?; + let v = req.into_inner(); + Ok(Response::new(status_wire( + self.authority + .status(&v.connection_id, &reference(required(v.reference)?)?) + .await + .map_err(error)?, + ))) + } + async fn revoke(&self, req: Request) -> RpcResult { + let _permit = self.check(&req, req.get_ref().protocol)?; + let v = req.into_inner(); + actor(&v.actor_id)?; + Ok(Response::new(status_wire( + self.authority + .revoke(&status(required(v.expected)?)?) + .await + .map_err(error)?, + ))) + } + async fn lease(&self, req: Request) -> RpcResult { + let _permit = self.check(&req, req.get_ref().protocol)?; + let v = req.into_inner(); + let i = required(v.worker)?; + let a = required(v.admission)?; + let scope = match required(v.scope)? { + wire::lease_request::Scope::Administration(s) => worker::WorkerScope::Administration { + request_id: s.request_id, + actor_id: s.actor_id, + }, + wire::lease_request::Scope::Execution(s) => worker::WorkerScope::Execution { + session_id: s.session_id, + run_id: s.run_id, + tool_call_id: s.tool_call_id, + binding_hash: s.binding_hash, + }, + }; + let lease = domain::ResourceLease { + context: context(v.context)?, + reference: reference(required(v.reference)?)?, + worker: worker::WorkerIdentity { + protocol: i.protocol, + boot_id: i.boot_id, + zone_id: i.zone_id, + policy_digest: i.policy_digest, + policy_revision: i.policy_revision, + }, + intent_id: v.intent_id, + request_digest: v.request_digest, + operation: v.operation, + expires_at: v.expires_at, + admission: worker::WorkerLease { + id: a.id, + deployment_id: a.deployment_id, + expires_at: a.expires_at, + }, + scope, + }; + let credential = self.authority.lease(&lease).await.map_err(error)?; + Ok(Response::new(wire::LeaseResponse { + protocol: wire::PROTOCOL, + header: credential.header, + value: credential.value, + expires_at: lease.expires_at, + intent_id: lease.intent_id, + })) + } +} diff --git a/crates/engram-coordinator/src/mcp/io.rs b/crates/engram-coordinator/src/mcp/io.rs new file mode 100644 index 000000000..6bc942702 --- /dev/null +++ b/crates/engram-coordinator/src/mcp/io.rs @@ -0,0 +1,125 @@ +//! Bounded worker I/O with durable deployment admission and refresh dispatch fencing. +use super::*; +use chrono::Duration; +use engram_core::types::mcp::{McpOAuthAdmission, McpRefreshClaim}; +use serde_json::{json, Value}; + +impl McpAuthority { + pub(super) async fn oauth_io( + &self, + context: &AuthorityContext, + destination: &Destination, + actor: &str, + mut operation: WorkerOperation, + secret: Option<&McpSecretReservation>, + claim: Option<&McpRefreshClaim>, + ) -> Result { + self.enabled()?; + if actor.is_empty() || actor.len() > 256 { + return Err(McpError::Invalid); + } + let now = self.manager.clock.now_utc(); + let deadline = claim + .map(|c| c.expires_at - Duration::seconds(5)) + .unwrap_or(now + Duration::seconds(10)) + .min(now + Duration::seconds(10)); + if deadline <= now { + return Err(McpError::Stale); + } + let admission = McpOAuthAdmission { + id: self.manager.entropy.uuid(), + deployment_id: self.registry.deployment_id.clone(), + connection_id: context.connection_id.clone(), + expires_at: deadline + Duration::seconds(5), + }; + if !self + .manager + .meta + .acquire_mcp_oauth_admission(&admission) + .await? + { + return Err(McpError::Unavailable); + } + let result = async { + let identity = self.identity(destination, None).await?; + if let Some(secret) = secret { + let payload = self.open_secret(secret).await?; + if payload.revoked + || secret.role != McpSecretRole::ClientSecret + || payload.context.connection_id != context.connection_id + || payload.context.config_revision != context.config_revision + { + return Err(McpError::Stale); + } + match &mut operation { + WorkerOperation::Token { grant, .. } => match grant { + TokenGrant::AuthorizationCode { client_secret, .. } + | TokenGrant::RefreshToken { client_secret, .. } => { + *client_secret = Some(payload.value); + } + }, + _ => return Err(McpError::Denied), + } + } + let intent = WorkerIntent { + id: self.manager.entropy.uuid().to_string(), + boot_id: identity.boot_id.clone(), + zone_id: identity.zone_id.clone(), + policy_digest: identity.policy_digest.clone(), + connection_id: context.connection_id.clone(), + connection_revision: context.config_revision, + credential_identity: context.credential_identity.clone(), + scope: WorkerScope::Administration { + request_id: admission.id.to_string(), + actor_id: actor.into(), + }, + operation: operation.kind().into(), + destination: destination.endpoint.clone(), + request_digest: digest(&json!({ + "context": context, + "admission": admission.id.to_string(), + "operation": operation.kind(), + "destination": destination, + }))?, + operation_digest: digest( + &serde_json::to_value(&operation).map_err(|_| McpError::Invalid)?, + )?, + expires_at: deadline.timestamp_millis(), + lease: WorkerLease { + id: admission.id.to_string(), + deployment_id: admission.deployment_id.clone(), + expires_at: admission.expires_at.timestamp_millis(), + }, + }; + // Admission, worker identity, and client-secret checks precede the + // irreversible rotating-token dispatch claim. + if self.manager.clock.now_utc() >= deadline { + return Err(McpError::Stale); + } + if let Some(claim) = claim { + if !self.manager.meta.claim_mcp_refresh(claim).await? { + return Err(McpError::Unavailable); + } + } + // A slow claim transaction must not extend the token RPC deadline. + let remaining = deadline - self.manager.clock.now_utc(); + if remaining <= Duration::zero() { + return Err(McpError::Stale); + } + tokio::time::timeout( + remaining.to_std().map_err(|_| McpError::Stale)?, + self.worker(destination)? + .rpc(&identity, &intent, &operation), + ) + .await + .map_err(|_| McpError::Transport)? + } + .await; + // Release only after the bounded RPC has ended. Expiry covers a crashed caller. + self.manager + .meta + .release_mcp_oauth_admission(&admission) + .await?; + result + } +} diff --git a/crates/engram-coordinator/src/mcp/lifecycle.rs b/crates/engram-coordinator/src/mcp/lifecycle.rs new file mode 100644 index 000000000..18c959cbc --- /dev/null +++ b/crates/engram-coordinator/src/mcp/lifecycle.rs @@ -0,0 +1,116 @@ +//! Reference-only status and expected-version revocation. +use super::*; +use engram_core::types::oauth::{OAuthCredentialKey, OAuthSubjectKind}; +impl McpAuthority { + pub async fn status( + &self, + connection: &str, + reference: &CredentialReference, + ) -> Result { + self.enabled()?; + match reference { + CredentialReference::ApiKey { reservation } + | CredentialReference::ClientSecret { reservation } => { + let current = self + .manager + .meta + .get_mcp_org_secret_reservation(&reservation.name) + .await? + .ok_or(McpError::Unavailable)?; + if current.connection_id != connection || current.role != reservation.role { + return Err(McpError::Denied); + } + let p = self.open_secret(¤t).await?; + let reference = if current.role == McpSecretRole::ApiKey { + CredentialReference::ApiKey { + reservation: current.clone(), + } + } else { + CredentialReference::ClientSecret { + reservation: current.clone(), + } + }; + Ok(CredentialStatus { + context: p.context, + reference, + version: current.generation, + status: if p.revoked { "revoked" } else { "connected" }.into(), + }) + } + CredentialReference::OAuth { registration_id } => { + let row = self + .manager + .meta + .get_oauth_credential(&OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: connection.into(), + provider: registration_id.clone(), + }) + .await? + .ok_or(McpError::Unavailable)?; + let bundle = self.decode_bundle(&row).await?; + Ok(CredentialStatus { + context: bundle.context, + reference: reference.clone(), + version: row.version, + status: row.status(self.manager.clock.now_utc()).as_str().into(), + }) + } + } + } + pub async fn revoke(&self, expected: &CredentialStatus) -> Result { + self.enabled()?; + if &self + .status(&expected.context.connection_id, &expected.reference) + .await? + != expected + { + return Err(McpError::Stale); + } + match &expected.reference { + CredentialReference::ApiKey { reservation } + | CredentialReference::ClientSecret { reservation } => { + let mut p = self.open_secret(reservation).await?; + p.revoked = true; + p.value.clear(); + p.reservation.generation = p + .reservation + .generation + .checked_add(1) + .ok_or(McpError::Invalid)?; + p.context.authorization_generation = p + .context + .authorization_generation + .checked_add(1) + .ok_or(McpError::Invalid)?; + p.context.credential_identity = self.random()?; + let sealed = crate::org_secrets::seal_org_secret( + self.manager.kek.as_ref(), + &reservation.name, + &serde_json::to_vec(&p).map_err(|_| McpError::Invalid)?, + ) + .await + .map_err(|_| McpError::Crypto)?; + self.manager + .meta + .put_mcp_org_secret(sealed, reservation) + .await?; + } + CredentialReference::OAuth { registration_id } => { + self.manager + .meta + .revoke_oauth_credential( + &OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: expected.context.connection_id.clone(), + provider: registration_id.clone(), + }, + expected.version, + ) + .await?; + } + } + self.status(&expected.context.connection_id, &expected.reference) + .await + } +} diff --git a/crates/engram-coordinator/src/mcp/mod.rs b/crates/engram-coordinator/src/mcp/mod.rs new file mode 100644 index 000000000..2a0997833 --- /dev/null +++ b/crates/engram-coordinator/src/mcp/mod.rs @@ -0,0 +1,365 @@ +//! Dedicated coordinator credential authority. Never registered on app gRPC. +pub mod worker; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum McpError { + Invalid, + Denied, + Unavailable, + Stale, + Crypto, + Transport, +} +pub type Result = std::result::Result; +impl From for McpError { + fn from(_: engram_core::MetaError) -> Self { + Self::Stale + } +} + +pub mod control; +mod io; +mod lifecycle; +pub mod model; +pub mod oauth; +pub mod startup; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use engram_core::types::org_secret::{McpSecretReservation, McpSecretRole}; +use engram_crypto::{CredCipher, SealedCred}; +use model::*; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, sync::Arc}; +use worker::*; + +pub trait SecurityEntropy: Send + Sync { + fn bytes(&self) -> Result<[u8; 32]>; +} +pub struct SystemSecurityEntropy; +impl SecurityEntropy for SystemSecurityEntropy { + fn bytes(&self) -> Result<[u8; 32]> { + engram_crypto::security_random_bytes().map_err(|_| McpError::Crypto) + } +} +pub struct McpAuthority { + pub(crate) manager: Arc, + pub(crate) security: Arc, + pub(crate) registry: Registry, + workers: BTreeMap>, +} +#[derive(Serialize, Deserialize)] +struct SecretPayload { + v: u32, + context: AuthorityContext, + reservation: McpSecretReservation, + header: String, + value: String, + revoked: bool, +} +impl McpAuthority { + pub fn new( + manager: Arc, + security: Arc, + registry: Registry, + workers: BTreeMap>, + ) -> Result { + if registry.deployment_id.is_empty() || registry.deployment_id.len() > 256 { + return Err(McpError::Invalid); + } + let this = Self { + manager, + security, + registry, + workers, + }; + for (id, zone) in &this.registry.zones { + if !this.workers.contains_key(id) + || zone.policy_digest.len() != 64 + || zone.policy_revision.is_empty() + { + return Err(McpError::Invalid); + } + for endpoint in &zone.endpoints { + Self::canonical_endpoint(endpoint)?; + } + } + for (id, r) in &this.registry.registrations { + if id != &r.id + || r.client_id.is_empty() + || r.client_id.len() > 8192 + || r.scopes + .iter() + .any(|s| s.is_empty() || s.len() > 256 || s.chars().any(char::is_whitespace)) + || r.scopes.len() > 128 + { + return Err(McpError::Invalid); + } + Self::canonical_endpoint(&r.issuer)?; + Self::canonical_endpoint(&r.redirect_uri)?; + for d in [ + &r.resource, + &r.resource_metadata, + &r.authorization_metadata, + &r.authorization, + &r.token, + ] { + this.destination(d)?; + } + } + Ok(this) + } + pub(crate) fn enabled(&self) -> Result<()> { + if !self.registry.enabled { + Err(McpError::Unavailable) + } else { + Ok(()) + } + } + pub(crate) fn random(&self) -> Result { + Ok(URL_SAFE_NO_PAD.encode(self.security.bytes()?)) + } + pub(crate) fn canonical_endpoint(endpoint: &str) -> Result<()> { + let url = url::Url::parse(endpoint).map_err(|_| McpError::Invalid)?; + if url.scheme() != "https" + || !matches!(url.host(), Some(url::Host::Domain(_))) + || !url.username().is_empty() + || url.password().is_some() + || url.query().is_some() + || url.fragment().is_some() + || url.as_str() != endpoint + { + return Err(McpError::Invalid); + } + Ok(()) + } + pub(crate) fn destination(&self, d: &Destination) -> Result<&Zone> { + let z = self + .registry + .zones + .get(&d.zone_id) + .ok_or(McpError::Denied)?; + if !z.endpoints.contains(&d.endpoint) { + return Err(McpError::Denied); + } + Ok(z) + } + pub(crate) fn worker(&self, d: &Destination) -> Result<&Arc> { + self.destination(d)?; + self.workers.get(&d.zone_id).ok_or(McpError::Denied) + } + pub(crate) async fn identity( + &self, + d: &Destination, + expected: Option<&WorkerIdentity>, + ) -> Result { + let z = self.destination(d)?; + let actual = self.worker(d)?.identity().await?; + if actual.protocol != 1 + || actual.zone_id != d.zone_id + || actual.policy_digest != z.policy_digest + || actual.policy_revision != z.policy_revision + || expected.is_some_and(|v| v != &actual) + { + return Err(McpError::Stale); + } + Ok(actual) + } + pub(crate) fn context(&self, change: &ChangeContext) -> Result { + self.enabled()?; + self.destination(&change.resource)?; + if change.connection_id.is_empty() + || change.connection_id.len() > 256 + || change.config_revision < 1 + || change.config_revision > 9_007_199_254_740_991 + || change.previous_authorization_generation < 0 + { + return Err(McpError::Invalid); + } + let generation = change + .previous_authorization_generation + .checked_add(1) + .filter(|v| *v <= 9_007_199_254_740_991) + .ok_or(McpError::Invalid)?; + let zone = self.destination(&change.resource)?; + Ok(AuthorityContext { + policy_digest: zone.policy_digest.clone(), + policy_revision: zone.policy_revision.clone(), + connection_id: change.connection_id.clone(), + config_revision: change.config_revision, + authorization_generation: generation, + credential_identity: self.random()?, + resource: change.resource.clone(), + }) + } + pub async fn put_secret( + &self, + change: &ChangeContext, + role: McpSecretRole, + expected: Option<&CredentialReference>, + header: &str, + value: &str, + ) -> Result { + let context = self.context(change)?; + if value.is_empty() + || value.len() > 8192 + || value.contains(['\r', '\n']) + || !["authorization", "x-api-key", "api-key"].contains(&header) + { + return Err(McpError::Invalid); + } + let reservation = match expected { + None => { + self.manager + .meta + .reserve_mcp_org_secret( + &format!("mcp-{}", self.random()?), + &context.connection_id, + role, + ) + .await? + } + Some( + CredentialReference::ApiKey { reservation } + | CredentialReference::ClientSecret { reservation }, + ) => { + if reservation.connection_id != context.connection_id || reservation.role != role { + return Err(McpError::Denied); + } + let previous = self.open_secret(reservation).await?; + if previous.context.authorization_generation + != change.previous_authorization_generation + { + return Err(McpError::Stale); + } + reservation.clone() + } + _ => return Err(McpError::Denied), + }; + let mut next = reservation.clone(); + next.generation = next.generation.checked_add(1).ok_or(McpError::Invalid)?; + let payload = SecretPayload { + v: 1, + context: context.clone(), + reservation: next.clone(), + header: header.into(), + value: value.into(), + revoked: false, + }; + let bytes = serde_json::to_vec(&payload).map_err(|_| McpError::Invalid)?; + let sealed = crate::org_secrets::seal_org_secret( + self.manager.kek.as_ref(), + &reservation.name, + &bytes, + ) + .await + .map_err(|_| McpError::Crypto)?; + self.manager + .meta + .put_mcp_org_secret(sealed, &reservation) + .await?; + let reference = match role { + McpSecretRole::ApiKey => CredentialReference::ApiKey { + reservation: next.clone(), + }, + McpSecretRole::ClientSecret => CredentialReference::ClientSecret { + reservation: next.clone(), + }, + }; + Ok(CredentialStatus { + context, + reference, + version: next.generation, + status: "connected".into(), + }) + } + async fn open_secret(&self, expected: &McpSecretReservation) -> Result { + let row = self + .manager + .meta + .get_org_secret_sealed(&expected.name) + .await? + .ok_or(McpError::Unavailable)?; + // Read the current permanent reservation after the captured ciphertext. + if self + .manager + .meta + .get_mcp_org_secret_reservation(&expected.name) + .await? + .as_ref() + != Some(expected) + { + return Err(McpError::Stale); + } + let envelope = SealedCred { + wrapped_dek: row.wrapped_dek, + nonce: row.nonce.try_into().map_err(|_| McpError::Crypto)?, + ciphertext: row.ciphertext, + key_id: row.key_id, + }; + let bytes = CredCipher::new(self.manager.kek.as_ref()) + .open(&envelope) + .await + .map_err(|_| McpError::Crypto)?; + let payload: SecretPayload = + serde_json::from_slice(&bytes).map_err(|_| McpError::Invalid)?; + if payload.v != 1 + || &payload.reservation != expected + || payload.context.connection_id != expected.connection_id + { + return Err(McpError::Denied); + } + Ok(payload) + } + pub async fn lease(&self, lease: &ResourceLease) -> Result { + self.enabled()?; + let now = self.manager.clock.now_utc().timestamp_millis(); + if lease.expires_at <= now + || lease.expires_at > now + 10000 + || lease.admission.expires_at < lease.expires_at + || lease.admission.expires_at > now + 300000 + || lease.admission.deployment_id != self.registry.deployment_id + || lease.admission.id.is_empty() + || lease.intent_id.is_empty() + || lease.request_digest.len() != 64 + || !lease.request_digest.bytes().all(|b| b.is_ascii_hexdigit()) + { + return Err(McpError::Denied); + } + if !matches!( + (&*lease.operation, &lease.scope), + ("discover", WorkerScope::Administration { .. }) + | ("execute", WorkerScope::Execution { .. }) + ) { + return Err(McpError::Denied); + } + if lease.context.policy_digest != lease.worker.policy_digest + || lease.context.policy_revision != lease.worker.policy_revision + { + return Err(McpError::Stale); + } + self.identity(&lease.context.resource, Some(&lease.worker)) + .await?; + let credential = match &lease.reference { + CredentialReference::ApiKey { reservation } => { + if reservation.role != McpSecretRole::ApiKey { + return Err(McpError::Denied); + } + let p = self.open_secret(reservation).await?; + if p.context != lease.context || p.revoked { + return Err(McpError::Stale); + } + ProviderCredential { + header: p.header, + value: p.value, + } + } + CredentialReference::OAuth { registration_id } => { + self.oauth_lease(&lease.context, registration_id).await? + } + _ => return Err(McpError::Denied), + }; + if self.manager.clock.now_utc().timestamp_millis() >= lease.expires_at { + return Err(McpError::Stale); + } + Ok(credential) + } +} diff --git a/crates/engram-coordinator/src/mcp/model.rs b/crates/engram-coordinator/src/mcp/model.rs new file mode 100644 index 000000000..ea5c31439 --- /dev/null +++ b/crates/engram-coordinator/src/mcp/model.rs @@ -0,0 +1,98 @@ +//! Metadata shared with the trusted control caller. No credential values. +use super::worker::{WorkerIdentity, WorkerLease, WorkerScope}; +use engram_core::types::org_secret::McpSecretReservation; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Destination { + pub zone_id: String, + pub endpoint: String, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ChangeContext { + pub connection_id: String, + pub config_revision: i64, + pub previous_authorization_generation: i64, + pub resource: Destination, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AuthorityContext { + pub policy_digest: String, + pub policy_revision: String, + pub connection_id: String, + pub config_revision: i64, + pub authorization_generation: i64, + pub credential_identity: String, + pub resource: Destination, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum CredentialReference { + ApiKey { reservation: McpSecretReservation }, + ClientSecret { reservation: McpSecretReservation }, + OAuth { registration_id: String }, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CredentialStatus { + pub context: AuthorityContext, + pub reference: CredentialReference, + pub version: i64, + pub status: String, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ResourceLease { + pub context: AuthorityContext, + pub reference: CredentialReference, + pub worker: WorkerIdentity, + pub intent_id: String, + pub request_digest: String, + pub operation: String, + pub scope: WorkerScope, + pub expires_at: i64, + pub admission: WorkerLease, +} +// Raw values live only in an authenticated request or one operation's memory. +#[derive(Serialize, Deserialize)] +pub struct ProviderCredential { + pub header: String, + pub value: String, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Zone { + pub policy_digest: String, + pub policy_revision: String, + pub endpoints: BTreeSet, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ClientRegistration { + pub id: String, + pub client_id: String, + pub confidential: bool, + pub redirect_uri: String, + pub issuer: String, + pub resource: Destination, + pub resource_metadata: Destination, + pub authorization_metadata: Destination, + pub authorization: Destination, + pub token: Destination, + pub scopes: BTreeSet, + pub issuer_parameter_required: bool, + pub account_field: Option, +} +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Registry { + #[serde(default)] + pub enabled: bool, + pub deployment_id: String, + pub zones: BTreeMap, + pub registrations: BTreeMap, +} diff --git a/crates/engram-coordinator/src/mcp/oauth.rs b/crates/engram-coordinator/src/mcp/oauth.rs new file mode 100644 index 000000000..66c0fab18 --- /dev/null +++ b/crates/engram-coordinator/src/mcp/oauth.rs @@ -0,0 +1,586 @@ +//! Durable encrypted PKCE and strict, exclusive refresh for registered MCP clients. +use super::*; +use chrono::{DateTime, Duration, Utc}; +use engram_core::types::{ + mcp::{McpRefreshClaim, McpSealedState}, + oauth::*, +}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use uuid::Uuid; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct BegunAuthorization { + pub flow_id: Uuid, + pub context: AuthorityContext, + pub authorization_url: String, + pub expires_at: DateTime, +} +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Callback { + pub context: AuthorityContext, + pub registration_id: String, + pub flow_id: Uuid, + pub state: String, + pub redirect_uri: String, + pub issuer: Option, + pub code: String, + pub actor_id: String, +} +#[derive(Serialize, Deserialize)] +struct FlowState { + require_issuer: bool, + v: u32, + id: Uuid, + state: String, + verifier: String, + context: AuthorityContext, + registration: ClientRegistration, + policies: BTreeMap, + client_secret: Option, + expected_version: Option, +} +#[derive(Serialize, Deserialize)] +pub(crate) struct Bundle { + v: u32, + pub context: AuthorityContext, + registration: ClientRegistration, + policies: BTreeMap, + client_secret: Option, + access_token: String, + refresh_token: Option, + expires_at: Option>, + provider_account: Option, + identity_source: String, +} +fn key(connection: &str, registration: &str) -> OAuthCredentialKey { + OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: connection.into(), + provider: registration.into(), + } +} +impl McpAuthority { + fn policies(&self, r: &ClientRegistration) -> Result> { + [ + &r.resource, + &r.resource_metadata, + &r.authorization_metadata, + &r.authorization, + &r.token, + ] + .into_iter() + .map(|d| { + let z = self.destination(d)?; + Ok(( + d.zone_id.clone(), + (z.policy_digest.clone(), z.policy_revision.clone()), + )) + }) + .collect() + } + fn registration(&self, id: &str) -> Result<&ClientRegistration> { + self.registry.registrations.get(id).ok_or(McpError::Denied) + } + fn check_registration( + &self, + r: &ClientRegistration, + policies: &BTreeMap, + ) -> Result<()> { + if self.registration(&r.id)? != r || &self.policies(r)? != policies { + return Err(McpError::Stale); + } + Ok(()) + } + async fn seal_state(&self, value: &T) -> Result { + let bytes = serde_json::to_vec(value).map_err(|_| McpError::Invalid)?; + let s = CredCipher::new(self.manager.kek.as_ref()) + .seal(&bytes) + .await + .map_err(|_| McpError::Crypto)?; + Ok(McpSealedState { + wrapped_dek: s.wrapped_dek, + nonce: s.nonce.to_vec(), + ciphertext: s.ciphertext, + key_id: s.key_id, + }) + } + async fn open_state(&self, s: McpSealedState) -> Result { + let bytes = CredCipher::new(self.manager.kek.as_ref()) + .open(&SealedCred { + wrapped_dek: s.wrapped_dek, + nonce: s.nonce.try_into().map_err(|_| McpError::Crypto)?, + ciphertext: s.ciphertext, + key_id: s.key_id, + }) + .await + .map_err(|_| McpError::Crypto)?; + serde_json::from_slice(&bytes).map_err(|_| McpError::Invalid) + } + pub async fn begin( + &self, + change: &ChangeContext, + registration_id: &str, + secret: Option<&CredentialReference>, + expected_version: Option, + actor: &str, + sharing_consent: bool, + ) -> Result { + let context = self.context(change)?; + if !sharing_consent { + return Err(McpError::Denied); + } + let r = self.registration(registration_id)?.clone(); + if r.resource != context.resource { + return Err(McpError::Denied); + } + let client_secret = match (r.confidential, secret) { + (false, None) => None, + (true, Some(CredentialReference::ClientSecret { reservation })) + if reservation.connection_id == context.connection_id + && reservation.role == McpSecretRole::ClientSecret => + { + let payload = self.open_secret(reservation).await?; + if payload.revoked { + return Err(McpError::Denied); + } + Some(reservation.clone()) + } + _ => return Err(McpError::Denied), + }; + let current = self + .manager + .meta + .get_oauth_credential(&key(&context.connection_id, registration_id)) + .await?; + if current.as_ref().map(|r| r.version) != expected_version { + return Err(McpError::Stale); + } + let resource = self + .oauth_io( + &context, + &r.resource_metadata, + actor, + WorkerOperation::Metadata { + endpoint: r.resource_metadata.endpoint.clone(), + }, + None, + None, + ) + .await?; + if resource["resource"] != r.resource.endpoint + || !resource["authorization_servers"] + .as_array() + .is_some_and(|a| a.iter().any(|v| v == &r.issuer)) + { + return Err(McpError::Denied); + } + let metadata = self + .oauth_io( + &context, + &r.authorization_metadata, + actor, + WorkerOperation::Metadata { + endpoint: r.authorization_metadata.endpoint.clone(), + }, + None, + None, + ) + .await?; + if metadata["issuer"] != r.issuer + || metadata["authorization_endpoint"] != r.authorization.endpoint + || metadata["token_endpoint"] != r.token.endpoint + || !metadata["code_challenge_methods_supported"] + .as_array() + .is_some_and(|a| a.iter().any(|v| v == "S256")) + { + return Err(McpError::Denied); + } + self.oauth_io( + &context, + &r.authorization, + actor, + WorkerOperation::ValidateDestination { + endpoint: r.authorization.endpoint.clone(), + }, + None, + None, + ) + .await?; + let now = self.manager.clock.now_utc(); + let expires_at = now + Duration::minutes(10); + let random = self.security.bytes()?; + let id = Uuid::from_bytes(random[..16].try_into().map_err(|_| McpError::Crypto)?); + let state = self.random()?; + let verifier = self.random()?; + let challenge = URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes())); + let flow = FlowState { + require_issuer: r.issuer_parameter_required + || metadata["authorization_response_iss_parameter_supported"] == true, + v: 1, + id, + state: state.clone(), + verifier, + context: context.clone(), + policies: self.policies(&r)?, + registration: r.clone(), + client_secret, + expected_version, + }; + self.manager + .meta + .create_mcp_oauth_flow( + OAuthFlow { + id, + key: key(&context.connection_id, registration_id), + owner_replica: self.manager.replica.clone(), + lease_expires_at: expires_at, + expires_at, + status: OAuthFlowStatus::Pending, + error_code: None, + created_at: now, + updated_at: now, + }, + self.seal_state(&flow).await?, + ) + .await?; + let mut url = url::Url::parse(&r.authorization.endpoint).map_err(|_| McpError::Invalid)?; + url.query_pairs_mut() + .append_pair("response_type", "code") + .append_pair("client_id", &r.client_id) + .append_pair("redirect_uri", &r.redirect_uri) + .append_pair("resource", &r.resource.endpoint) + .append_pair( + "scope", + &r.scopes.iter().cloned().collect::>().join(" "), + ) + .append_pair("state", &state) + .append_pair("code_challenge", &challenge) + .append_pair("code_challenge_method", "S256"); + Ok(BegunAuthorization { + flow_id: id, + context, + authorization_url: url.into(), + expires_at, + }) + } + pub async fn callback(&self, callback: &Callback) -> Result { + self.enabled()?; + // The winner removes ciphertext atomically BEFORE any token request. + let flow: FlowState = self + .open_state( + self.manager + .meta + .consume_mcp_oauth_flow( + callback.flow_id, + &key(&callback.context.connection_id, &callback.registration_id), + ) + .await?, + ) + .await?; + let result = async { + let r = &flow.registration; + if flow.v != 1 + || flow.id != callback.flow_id + || flow.context != callback.context + || flow.state != callback.state + || r.id != callback.registration_id + || r.redirect_uri != callback.redirect_uri + || callback.issuer.as_ref().is_some_and(|i| i != &r.issuer) + || (flow.require_issuer && callback.issuer.is_none()) + || callback.code.is_empty() + || callback.code.len() > 8192 + { + return Err(McpError::Denied); + } + self.check_registration(r, &flow.policies)?; + let response = self + .oauth_io( + &flow.context, + &r.token, + &callback.actor_id, + WorkerOperation::Token { + endpoint: r.token.endpoint.clone(), + grant: TokenGrant::AuthorizationCode { + code: callback.code.clone(), + redirect_uri: r.redirect_uri.clone(), + code_verifier: flow.verifier, + client_id: r.client_id.clone(), + resource: r.resource.endpoint.clone(), + client_secret: None, + }, + }, + flow.client_secret.as_ref(), + None, + ) + .await?; + let grant = validate_token(&response, r, None, self.manager.clock.now_utc())?; + let bundle = Bundle { + v: 1, + context: flow.context, + registration: flow.registration, + policies: flow.policies, + client_secret: flow.client_secret, + access_token: grant.access_token, + refresh_token: grant.refresh_token, + expires_at: grant.expires_at, + identity_source: if grant.account.is_some() { + "provider_response" + } else { + "authorization_ceremony" + } + .into(), + provider_account: grant.account, + }; + let sealed = self.credential(&bundle).await?; + let row = self + .manager + .meta + .put_oauth_credential(sealed, flow.expected_version) + .await?; + Ok(status(&bundle, &row)) + } + .await; + // Completion metadata cannot make a consumed flow replayable. + let _ = self + .manager + .meta + .finish_oauth_flow_unowned( + callback.flow_id, + if result.is_ok() { + OAuthFlowStatus::Succeeded + } else { + OAuthFlowStatus::Failed + }, + if result.is_err() { + Some("authorization_failed") + } else { + None + }, + ) + .await; + result + } + async fn credential(&self, bundle: &Bundle) -> Result { + let sealed = self.seal_state(bundle).await?; + Ok(NewSealedOAuthCredential { + key: key(&bundle.context.connection_id, &bundle.registration.id), + wrapped_dek: sealed.wrapped_dek, + nonce: sealed.nonce, + ciphertext: sealed.ciphertext, + key_id: sealed.key_id, + metadata: OAuthAccountMetadata { + account_id: bundle.context.credential_identity.clone(), + ..Default::default() + }, + expires_at: bundle.expires_at, + }) + } + pub(crate) async fn decode_bundle(&self, row: &SealedOAuthCredential) -> Result { + let bytes = self.manager.open(row).await.map_err(|_| McpError::Crypto)?; + let bundle: Bundle = serde_json::from_slice(&bytes).map_err(|_| McpError::Invalid)?; + if bundle.v != 1 + || row.key != key(&bundle.context.connection_id, &bundle.registration.id) + || row.metadata.account_id != bundle.context.credential_identity + { + return Err(McpError::Denied); + } + Ok(bundle) + } + pub(crate) async fn open_bundle(&self, row: &SealedOAuthCredential) -> Result { + let bundle = self.decode_bundle(row).await?; + self.check_registration(&bundle.registration, &bundle.policies)?; + if let Some(s) = &bundle.client_secret { + if self + .manager + .meta + .get_mcp_org_secret_reservation(&s.name) + .await? + .as_ref() + != Some(s) + { + return Err(McpError::Stale); + } + } + Ok(bundle) + } + pub(crate) async fn oauth_lease( + &self, + expected: &AuthorityContext, + registration_id: &str, + ) -> Result { + let key = key(&expected.connection_id, registration_id); + let mut row = self + .manager + .meta + .get_oauth_credential(&key) + .await? + .ok_or(McpError::Unavailable)?; + // Bounded follower wait. Never reacquire or retry the rotating exchange. + for _ in 0..20 { + if row.broken_reason.as_deref() != Some("refresh_pending") { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + row = self + .manager + .meta + .get_oauth_credential(&key) + .await? + .ok_or(McpError::Unavailable)?; + } + if row.revoked_at.is_some() || row.broken_at.is_some() { + return Err(McpError::Unavailable); + } + let mut bundle = self.open_bundle(&row).await?; + if &bundle.context != expected { + return Err(McpError::Stale); + } + if bundle + .expires_at + .is_some_and(|t| t <= self.manager.clock.now_utc()) + { + let claim = McpRefreshClaim { + id: self.manager.entropy.uuid(), + key: key.clone(), + version: row.version, + expires_at: self.manager.clock.now_utc() + Duration::seconds(25), + }; + let refresh_token = bundle.refresh_token.take().ok_or(McpError::Unavailable)?; + let response = self + .oauth_io( + &bundle.context, + &bundle.registration.token, + "credential-refresh", + WorkerOperation::Token { + endpoint: bundle.registration.token.endpoint.clone(), + grant: TokenGrant::RefreshToken { + refresh_token: refresh_token.clone(), + client_id: bundle.registration.client_id.clone(), + resource: bundle.registration.resource.endpoint.clone(), + client_secret: None, + }, + }, + bundle.client_secret.as_ref(), + Some(&claim), + ) + .await?; + let grant = validate_token( + &response, + &bundle.registration, + bundle.provider_account.as_deref(), + self.manager.clock.now_utc(), + )?; + bundle.access_token = grant.access_token; + bundle.refresh_token = grant.refresh_token.or(Some(refresh_token)); + bundle.expires_at = grant.expires_at; + self.manager + .meta + .complete_mcp_refresh(self.credential(&bundle).await?, &claim) + .await?; + } + if bundle + .expires_at + .is_some_and(|t| t <= self.manager.clock.now_utc()) + { + return Err(McpError::Unavailable); + } + Ok(ProviderCredential { + header: "authorization".into(), + value: format!("Bearer {}", bundle.access_token), + }) + } +} +fn status(bundle: &Bundle, row: &SealedOAuthCredential) -> CredentialStatus { + CredentialStatus { + context: bundle.context.clone(), + reference: CredentialReference::OAuth { + registration_id: bundle.registration.id.clone(), + }, + version: row.version, + status: if row.revoked_at.is_some() { + "revoked" + } else if row.broken_at.is_some() { + "broken" + } else { + "connected" + } + .into(), + } +} +struct ValidatedToken { + access_token: String, + refresh_token: Option, + expires_at: Option>, + account: Option, +} +fn token_text(v: &Value) -> Result { + let s = v.as_str().ok_or(McpError::Invalid)?; + if s.is_empty() || s.len() > 8192 || s.contains(['\r', '\n']) { + return Err(McpError::Invalid); + } + Ok(s.into()) +} +fn validate_token( + value: &Value, + r: &ClientRegistration, + account: Option<&str>, + now: DateTime, +) -> Result { + if !value.is_object() + || value.get("error").is_some() + || !value["token_type"] + .as_str() + .is_some_and(|s| s.eq_ignore_ascii_case("bearer")) + { + return Err(McpError::Denied); + } + for (field, expected) in [ + ("iss", &r.issuer), + ("issuer", &r.issuer), + ("resource", &r.resource.endpoint), + ("audience", &r.resource.endpoint), + ] { + if value.get(field).is_some_and(|v| v != expected) { + return Err(McpError::Denied); + } + } + if let Some(scope) = value.get("scope") { + let s = scope.as_str().ok_or(McpError::Invalid)?; + let scopes: std::collections::BTreeSet<_> = + s.split_whitespace().map(str::to_owned).collect(); + if scopes != r.scopes { + return Err(McpError::Denied); + } + } + let provider_account = r + .account_field + .as_ref() + .and_then(|f| value.get(f)) + .map(token_text) + .transpose()?; + if provider_account + .as_ref() + .is_some_and(|p| p.len() > 512 || account.is_some_and(|a| a != p)) + { + return Err(McpError::Denied); + } + let expires_at = value + .get("expires_in") + .map(|v| { + let n = v + .as_i64() + .filter(|n| *n > 0 && *n <= 31_536_000) + .ok_or(McpError::Invalid)?; + Ok::<_, McpError>(now + Duration::seconds(n)) + }) + .transpose()?; + Ok(ValidatedToken { + access_token: token_text(&value["access_token"])?, + refresh_token: value.get("refresh_token").map(token_text).transpose()?, + expires_at, + account: provider_account, + }) +} diff --git a/crates/engram-coordinator/src/mcp/startup.rs b/crates/engram-coordinator/src/mcp/startup.rs new file mode 100644 index 000000000..06593174b --- /dev/null +++ b/crates/engram-coordinator/src/mcp/startup.rs @@ -0,0 +1,103 @@ +//! Operator-owned files configure the dedicated listener and independent zones. +use super::{ + control::{self, ControlTls}, + model::Registry, + worker::{WorkerClient, WorkerTransport}, + McpAuthority, McpError, SystemSecurityEntropy, +}; +use serde::Deserialize; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + sync::Arc, +}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct OperatorConfig { + registry: Registry, + listen_addr: std::net::SocketAddr, + server_certificate_file: PathBuf, + server_private_key_file: PathBuf, + orchestrator_ca_file: PathBuf, + orchestrator_fingerprints: Vec, + workers: BTreeMap, +} +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct WorkerFiles { + url: String, + server_ca_file: PathBuf, + client_identity_file: PathBuf, +} +async fn bounded_file(path: &Path) -> super::Result> { + if tokio::fs::metadata(path) + .await + .map_err(|_| McpError::Invalid)? + .len() + > 128 * 1024 + { + return Err(McpError::Invalid); + } + let bytes = tokio::fs::read(path).await.map_err(|_| McpError::Invalid)?; + if bytes.len() > 128 * 1024 { + return Err(McpError::Invalid); + } + Ok(bytes) +} +pub async fn start( + path: Option<&Path>, + manager: Arc, + mut shutdown: tokio::sync::watch::Receiver, +) -> super::Result>> { + let Some(path) = path else { return Ok(None) }; + let config: OperatorConfig = + serde_json::from_slice(&bounded_file(path).await?).map_err(|_| McpError::Invalid)?; + if !config.registry.enabled { + return Ok(None); + } + let mut workers = BTreeMap::new(); + for (id, files) in config.workers { + let client = WorkerClient::new( + &files.url, + &bounded_file(&files.server_ca_file).await?, + &bounded_file(&files.client_identity_file).await?, + )?; + workers.insert(id, Arc::new(client) as Arc); + } + let authority = Arc::new(McpAuthority::new( + manager, + Arc::new(SystemSecurityEntropy), + config.registry, + workers, + )?); + let tls = ControlTls { + certificate: bounded_file(&config.server_certificate_file).await?, + private_key: bounded_file(&config.server_private_key_file).await?, + client_ca: bounded_file(&config.orchestrator_ca_file).await?, + orchestrator_fingerprints: config.orchestrator_fingerprints, + }; + let router = control::server(authority, tls)?; + let listener = tokio::net::TcpListener::bind(config.listen_addr) + .await + .map_err(|_| McpError::Unavailable)?; + Ok(Some(tokio::spawn(async move { + let stop = async move { + while !*shutdown.borrow_and_update() { + if shutdown.changed().await.is_err() { + break; + } + } + }; + if router + .serve_with_incoming_shutdown( + tonic::transport::server::TcpIncoming::from(listener), + stop, + ) + .await + .is_err() + { + tracing::error!("MCP control listener stopped"); + } + }))) +} diff --git a/crates/engram-coordinator/src/mcp/worker.rs b/crates/engram-coordinator/src/mcp/worker.rs new file mode 100644 index 000000000..5d4887713 --- /dev/null +++ b/crates/engram-coordinator/src/mcp/worker.rs @@ -0,0 +1,275 @@ +//! Worker protocol 1. Operations are fixed; no provider HTTP proxy is exposed. +use super::{McpError, Result}; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use std::time::Duration; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorkerIdentity { + pub protocol: u32, + pub boot_id: String, + pub zone_id: String, + pub policy_digest: String, + pub policy_revision: String, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)] +pub enum WorkerScope { + #[serde(rename_all = "camelCase")] + Administration { + request_id: String, + actor_id: String, + }, + #[serde(rename_all = "camelCase")] + Execution { + session_id: String, + run_id: String, + tool_call_id: String, + binding_hash: String, + }, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorkerLease { + pub id: String, + pub deployment_id: String, + pub expires_at: i64, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct WorkerIntent { + pub id: String, + pub boot_id: String, + pub zone_id: String, + pub policy_digest: String, + pub connection_id: String, + pub connection_revision: i64, + pub credential_identity: String, + pub scope: WorkerScope, + pub operation: String, + pub destination: String, + pub request_digest: String, + pub operation_digest: String, + pub expires_at: i64, + pub lease: WorkerLease, +} +// These types intentionally have no Debug implementation: token fields are private. +#[derive(Serialize)] +#[serde(tag = "kind", rename_all = "camelCase")] +pub enum WorkerOperation { + Metadata { endpoint: String }, + ValidateDestination { endpoint: String }, + Token { endpoint: String, grant: TokenGrant }, +} +#[derive(Serialize)] +#[serde(tag = "grant_type")] +pub enum TokenGrant { + #[serde(rename = "authorization_code")] + AuthorizationCode { + code: String, + redirect_uri: String, + code_verifier: String, + client_id: String, + resource: String, + #[serde(skip_serializing_if = "Option::is_none")] + client_secret: Option, + }, + #[serde(rename = "refresh_token")] + RefreshToken { + refresh_token: String, + client_id: String, + resource: String, + #[serde(skip_serializing_if = "Option::is_none")] + client_secret: Option, + }, +} +impl WorkerOperation { + pub fn kind(&self) -> &'static str { + match self { + Self::Metadata { .. } => "metadata", + Self::ValidateDestination { .. } => "validateDestination", + Self::Token { .. } => "token", + } + } + pub fn endpoint(&self) -> &str { + match self { + Self::Metadata { endpoint } + | Self::ValidateDestination { endpoint } + | Self::Token { endpoint, .. } => endpoint, + } + } +} + +/// This encoder is deliberately limited to the fixed coordinator wire: strings, +/// booleans, null and safe integers. No floating point enters these requests. +/// Keys follow JavaScript UTF-16 ordering, including non-BMP names in vectors. +pub fn canonical(value: &Value) -> Result { + Ok(match value { + Value::Null => "null".into(), + Value::Bool(v) => v.to_string(), + Value::String(v) => serde_json::to_string(v).map_err(|_| McpError::Invalid)?, + Value::Number(v) => { + let n = v.as_i64().ok_or(McpError::Invalid)?; + if !(-9_007_199_254_740_991..=9_007_199_254_740_991).contains(&n) { + return Err(McpError::Invalid); + } + n.to_string() + } + Value::Array(v) => format!( + "[{}]", + v.iter() + .map(canonical) + .collect::>>()? + .join(",") + ), + Value::Object(v) => { + let mut keys: Vec<_> = v.keys().collect(); + keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16())); + let fields = keys + .into_iter() + .map(|k| { + Ok(format!( + "{}:{}", + serde_json::to_string(k).map_err(|_| McpError::Invalid)?, + canonical(&v[k])? + )) + }) + .collect::>>()?; + format!("{{{}}}", fields.join(",")) + } + }) +} +pub fn digest(value: &Value) -> Result { + Ok(hex::encode(Sha256::digest(canonical(value)?.as_bytes()))) +} + +#[async_trait] +pub trait WorkerTransport: Send + Sync { + async fn identity(&self) -> Result; + async fn rpc( + &self, + identity: &WorkerIdentity, + intent: &WorkerIntent, + operation: &WorkerOperation, + ) -> Result; + async fn check_identity(&self, expected: &WorkerIdentity) -> Result<()> { + if &self.identity().await? != expected { + return Err(McpError::Stale); + } + Ok(()) + } +} +pub struct WorkerClient { + client: reqwest::Client, + base: String, +} +impl WorkerClient { + pub fn new(base: &str, ca: &[u8], identity: &[u8]) -> Result { + let url = url::Url::parse(base).map_err(|_| McpError::Invalid)?; + if url.scheme() != "https" + || url.host_str().is_none() + || !url.username().is_empty() + || url.password().is_some() + || url.query().is_some() + || url.fragment().is_some() + || url.path() != "/" + { + return Err(McpError::Invalid); + } + let certs = reqwest::Certificate::from_pem_bundle(ca).map_err(|_| McpError::Invalid)?; + if certs.is_empty() { + return Err(McpError::Invalid); + } + let client = engram_tls::client_builder() + .tls_certs_only(certs) + .identity(reqwest::Identity::from_pem(identity).map_err(|_| McpError::Invalid)?) + .tls_version_min(reqwest::tls::Version::TLS_1_3) + .https_only(true) + .no_proxy() + .redirect(reqwest::redirect::Policy::none()) + .retry(reqwest::retry::never()) + .connect_timeout(Duration::from_secs(5)) + .timeout(Duration::from_secs(10)) + .tls_sslkeylogfile(false) + .build() + .map_err(|_| McpError::Transport)?; + Ok(Self { + client, + base: base.trim_end_matches('/').into(), + }) + } + async fn json(response: reqwest::Response) -> Result { + if !response.status().is_success() { + return Err(McpError::Transport); + } + let mut response = response; + let mut bytes = Vec::new(); + while let Some(chunk) = response.chunk().await.map_err(|_| McpError::Transport)? { + if bytes.len() + chunk.len() > 2 * 1024 * 1024 { + return Err(McpError::Invalid); + } + bytes.extend_from_slice(&chunk); + } + serde_json::from_slice(&bytes).map_err(|_| McpError::Invalid) + } +} +#[async_trait] +impl WorkerTransport for WorkerClient { + async fn identity(&self) -> Result { + let v = Self::json( + self.client + .get(format!("{}/identity", self.base)) + .send() + .await + .map_err(|_| McpError::Transport)?, + ) + .await?; + let identity: WorkerIdentity = serde_json::from_value(v).map_err(|_| McpError::Invalid)?; + if identity.protocol != 1 || identity.boot_id.is_empty() { + return Err(McpError::Denied); + } + Ok(identity) + } + async fn rpc( + &self, + identity: &WorkerIdentity, + intent: &WorkerIntent, + operation: &WorkerOperation, + ) -> Result { + let op = serde_json::to_value(operation).map_err(|_| McpError::Invalid)?; + if intent.boot_id != identity.boot_id + || intent.zone_id != identity.zone_id + || intent.policy_digest != identity.policy_digest + || intent.operation != operation.kind() + || intent.destination != operation.endpoint() + || intent.operation_digest != digest(&op)? + { + return Err(McpError::Denied); + } + let body = serde_json::json!({"protocol":1,"intent":intent,"operation":op}); + let response = Self::json( + self.client + .post(format!("{}/rpc", self.base)) + .json(&body) + .send() + .await + .map_err(|_| McpError::Transport)?, + ) + .await?; + if response["ok"] != true + || response["identity"] + != serde_json::to_value(identity).map_err(|_| McpError::Invalid)? + || response["intent"] != body["intent"] + || response["intentId"] != intent.id + || response["requestDigest"] != intent.request_digest + || response["operationDigest"] != intent.operation_digest + || response["leaseId"] != intent.lease.id + { + return Err(McpError::Denied); + } + response.get("result").cloned().ok_or(McpError::Invalid) + } +} diff --git a/crates/engram-coordinator/src/oauth.rs b/crates/engram-coordinator/src/oauth.rs index ebb0453f5..cc6520d04 100644 --- a/crates/engram-coordinator/src/oauth.rs +++ b/crates/engram-coordinator/src/oauth.rs @@ -621,6 +621,11 @@ impl From for OAuthServiceError { } pub(crate) fn validate_key(key: &OAuthCredentialKey) -> Result<(), OAuthServiceError> { + if key.subject_kind == OAuthSubjectKind::Mcp { + return Err(OAuthServiceError::BadRequest( + "MCP credentials require the dedicated control service".into(), + )); + } // Provider charset matches the connector-slug rule (`new_relic` carries // an underscore). if key.subject_id.trim().is_empty() @@ -1068,6 +1073,22 @@ async fn read_bounded_private(path: &Path) -> Result, OAuthDriverError> #[cfg(test)] mod tests { + #[test] + fn generic_oauth_acquisition_rejects_mcp_subjects() { + for kind in [ + OAuthSubjectKind::User, + OAuthSubjectKind::Connector, + OAuthSubjectKind::UserConnector, + OAuthSubjectKind::Mcp, + ] { + let key = OAuthCredentialKey { + subject_kind: kind, + subject_id: "connection-a".into(), + provider: "registered-a".into(), + }; + assert_eq!(validate_key(&key).is_ok(), kind != OAuthSubjectKind::Mcp); + } + } use super::*; #[cfg(unix)] diff --git a/crates/engram-coordinator/src/oauth_refresh.rs b/crates/engram-coordinator/src/oauth_refresh.rs index e1f491103..237db81e3 100644 --- a/crates/engram-coordinator/src/oauth_refresh.rs +++ b/crates/engram-coordinator/src/oauth_refresh.rs @@ -392,6 +392,11 @@ impl OAuthManager { &self, key: &OAuthCredentialKey, ) -> Result { + if key.subject_kind == OAuthSubjectKind::Mcp { + return Err(OAuthServiceError::BadRequest( + "MCP credentials require the dedicated control service".into(), + )); + } let row = self .meta .get_oauth_credential(key) diff --git a/crates/engram-coordinator/tests/fixtures/mcp-worker.mjs b/crates/engram-coordinator/tests/fixtures/mcp-worker.mjs new file mode 100644 index 000000000..486f99d28 --- /dev/null +++ b/crates/engram-coordinator/tests/fixtures/mcp-worker.mjs @@ -0,0 +1,107 @@ +// Actual worker service, with only fixture DNS resolution replaced. +import { createWorker } from "../../../../mcp-worker/src/service.ts"; +import { ZonePolicy } from "../../../../mcp-worker/src/network.ts"; +import { mkdtempSync, readFileSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execFileSync } from "node:child_process"; +import { X509Certificate } from "node:crypto"; +const dir = mkdtempSync(join(tmpdir(), "mcp-coordinator-test-")); +const run = (args) => execFileSync("openssl", args, { stdio: "ignore" }); +run([ + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + join(dir, "ca-key"), + "-out", + join(dir, "ca"), + "-days", + "1", + "-subj", + "/CN=fixture-ca", + "-addext", + "basicConstraints=critical,CA:TRUE", +]); +run([ + "req", + "-new", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + join(dir, "key"), + "-out", + join(dir, "csr"), + "-subj", + "/CN=localhost", +]); +writeFileSync( + join(dir, "ext"), + "basicConstraints=critical,CA:FALSE\nsubjectAltName=DNS:localhost\nextendedKeyUsage=serverAuth,clientAuth\n", +); +run([ + "x509", + "-req", + "-in", + join(dir, "csr"), + "-CA", + join(dir, "ca"), + "-CAkey", + join(dir, "ca-key"), + "-CAcreateserial", + "-out", + join(dir, "cert"), + "-days", + "1", + "-extfile", + join(dir, "ext"), +]); +const key = readFileSync(join(dir, "key"), "utf8"), + cert = readFileSync(join(dir, "cert"), "utf8"), + ca = readFileSync(join(dir, "ca"), "utf8"); +rmSync(dir, { recursive: true }); +const zone = new ZonePolicy({ + id: "public", + revision: "1", + mode: "public", + authorities: ["auth.example"], + aliases: [], + cidrs: [], + denyCidrs: [], +}); +const worker = createWorker({ + zone, + tls: { key, cert, ca }, + callers: [ + { + fingerprint: new X509Certificate(cert).fingerprint256, + role: "coordinator", + }, + ], + enabled: true, + network: { + resolve: async () => ["8.8.8.8"], + request: () => { + throw Error("no_provider_dial"); + }, + }, +}); +const port = await worker.listen(0, "127.0.0.1"); +console.log( + JSON.stringify({ + port, + key, + cert, + ca, + fingerprint: new X509Certificate(cert).fingerprint256, + identity: worker.identity, + }), +); +process.stdin.resume(); +process.stdin.on("end", async () => { + await worker.close(); + process.exit(0); +}); diff --git a/crates/engram-coordinator/tests/mcp_authority.rs b/crates/engram-coordinator/tests/mcp_authority.rs new file mode 100644 index 000000000..d1ec8dfc1 --- /dev/null +++ b/crates/engram-coordinator/tests/mcp_authority.rs @@ -0,0 +1,276 @@ +use engram_coordinator::mcp::worker::{canonical, digest}; +use serde_json::json; + +#[test] +fn fixed_worker_wire_matches_node_utf16_and_integer_encoding() { + let value = json!({"\u{e000}":"last", "\u{10000}":"first", "line":"\n\u{000f}","n":9007199254740991i64}); + assert_eq!( + canonical(&value).unwrap(), + "{\"line\":\"\\n\\u000f\",\"n\":9007199254740991,\"𐀀\":\"first\",\"\":\"last\"}" + ); + assert_eq!( + digest(&json!({"kind":"metadata","endpoint":"https://auth.example/metadata"})).unwrap(), + "e776800d6d90b7a995cb193fbc359e5b248ed21f8c78519749d290599cef2b7d" + ); + assert!(canonical(&json!(9007199254740992i64)).is_err()); + assert!(canonical(&json!(1.1)).is_err()); +} + +#[tokio::test] +async fn actual_worker_mtls_identity_and_fixed_rpc() { + use engram_coordinator::mcp::worker::*; + use tokio::io::{AsyncBufReadExt, BufReader}; + let mut child = tokio::process::Command::new("node") + .arg(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/mcp-worker.mjs" + )) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .kill_on_drop(true) + .spawn() + .unwrap(); + let mut line = String::new(); + BufReader::new(child.stdout.take().unwrap()) + .read_line(&mut line) + .await + .unwrap(); + let fixture: serde_json::Value = serde_json::from_str(&line).unwrap(); + let url = format!("https://localhost:{}", fixture["port"]); + let cert = fixture["cert"].as_str().unwrap(); + let pem = format!("{}{}", cert, fixture["key"].as_str().unwrap()); + let client = WorkerClient::new( + &url, + fixture["ca"].as_str().unwrap().as_bytes(), + pem.as_bytes(), + ) + .unwrap(); + let identity = client.identity().await.unwrap(); + assert_eq!(identity.zone_id, "public"); + let no_cert = engram_tls::client_builder() + .tls_certs_only([reqwest::Certificate::from_pem( + fixture["ca"].as_str().unwrap().as_bytes(), + ) + .unwrap()]) + .no_proxy() + .build() + .unwrap(); + assert!(no_cert.get(format!("{url}/identity")).send().await.is_err()); + assert!(WorkerClient::new("http://localhost", cert.as_bytes(), pem.as_bytes()).is_err()); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + let operation = WorkerOperation::ValidateDestination { + endpoint: "https://auth.example/metadata".into(), + }; + let intent = WorkerIntent { + id: "intent-a".into(), + boot_id: identity.boot_id.clone(), + zone_id: identity.zone_id.clone(), + policy_digest: identity.policy_digest.clone(), + connection_id: "connection-a".into(), + connection_revision: 1, + credential_identity: "authority-a".into(), + scope: WorkerScope::Administration { + request_id: "request-a".into(), + actor_id: "actor-a".into(), + }, + operation: "validateDestination".into(), + destination: "https://auth.example/metadata".into(), + request_digest: "a".repeat(64), + operation_digest: digest(&serde_json::to_value(&operation).unwrap()).unwrap(), + expires_at: now + 10000, + lease: WorkerLease { + id: "lease-a".into(), + deployment_id: "deployment-a".into(), + expires_at: now + 10000, + }, + }; + client.rpc(&identity, &intent, &operation).await.unwrap(); + assert!(client.rpc(&identity, &intent, &operation).await.is_err()); + let mut wrong = identity.clone(); + wrong.boot_id = "wrong".into(); + assert!(client.check_identity(&wrong).await.is_err()); +} + +mod authority { + use engram_coordinator::{ + mcp::{model::*, worker::*, *}, + oauth::OAuthManager, + }; + use engram_core::{ + traits::{Clock, StaticSecretStore}, + types::org_secret::McpSecretRole, + }; + use engram_crypto::EnvVarKeyProvider; + use engram_sim::{ManualClock, SimEntropy, SimMetadataStore}; + use std::{ + collections::{BTreeMap, BTreeSet, HashMap}, + sync::{ + atomic::{AtomicU8, Ordering}, + Arc, Mutex, + }, + }; + struct DeterministicSecurity(AtomicU8); + impl SecurityEntropy for DeterministicSecurity { + fn bytes(&self) -> Result<[u8; 32]> { + Ok([self.0.fetch_add(1, Ordering::SeqCst); 32]) + } + } + struct FakeWorker(Mutex); + #[async_trait::async_trait] + impl WorkerTransport for FakeWorker { + async fn identity(&self) -> Result { + Ok(self.0.lock().unwrap().clone()) + } + async fn rpc( + &self, + _: &WorkerIdentity, + _: &WorkerIntent, + _: &WorkerOperation, + ) -> Result { + Err(McpError::Unavailable) + } + } + fn setup() -> ( + McpAuthority, + Arc, + WorkerIdentity, + Arc, + ) { + let clock = ManualClock::new(); + let entropy = Arc::new(SimEntropy::seeded(9)); + let meta = SimMetadataStore::new(clock.clone(), entropy.clone()); + let manager = OAuthManager::new( + meta, + Arc::new(EnvVarKeyProvider::from_bytes([8; 32], "test")), + clock.clone(), + entropy, + Arc::new(StaticSecretStore::new(HashMap::new())), + ); + let identity = WorkerIdentity { + protocol: 1, + boot_id: "boot-a".into(), + zone_id: "public".into(), + policy_digest: "a".repeat(64), + policy_revision: "1".into(), + }; + let registry = Registry { + enabled: true, + deployment_id: "deployment-a".into(), + zones: BTreeMap::from([( + "public".into(), + Zone { + policy_digest: identity.policy_digest.clone(), + policy_revision: "1".into(), + endpoints: BTreeSet::from(["https://resource.example/mcp".into()]), + }, + )]), + registrations: BTreeMap::new(), + }; + let worker = Arc::new(FakeWorker(Mutex::new(identity.clone()))); + ( + McpAuthority::new( + manager, + Arc::new(DeterministicSecurity(AtomicU8::new(1))), + registry, + BTreeMap::from([("public".into(), worker.clone() as Arc)]), + ) + .unwrap(), + clock, + identity, + worker, + ) + } + #[tokio::test] + async fn api_rotation_fences_old_lease_even_before_orchestrator_publication() { + let (authority, clock, identity, worker) = setup(); + let change = ChangeContext { + connection_id: "connection-a".into(), + config_revision: 1, + previous_authorization_generation: 0, + resource: Destination { + zone_id: "public".into(), + endpoint: "https://resource.example/mcp".into(), + }, + }; + let first = authority + .put_secret( + &change, + McpSecretRole::ApiKey, + None, + "x-api-key", + "first-secret", + ) + .await + .unwrap(); + let lease = ResourceLease { + context: first.context.clone(), + reference: first.reference.clone(), + worker: identity, + intent_id: "intent-a".into(), + request_digest: "a".repeat(64), + operation: "discover".into(), + scope: WorkerScope::Administration { + request_id: "request-a".into(), + actor_id: "actor-a".into(), + }, + expires_at: clock.now_utc().timestamp_millis() + 10000, + admission: WorkerLease { + id: "lease-a".into(), + deployment_id: "deployment-a".into(), + expires_at: clock.now_utc().timestamp_millis() + 10000, + }, + }; + assert_eq!(authority.lease(&lease).await.unwrap().value, "first-secret"); + let mut next = change; + next.previous_authorization_generation = first.context.authorization_generation; + let second = authority + .put_secret( + &next, + McpSecretRole::ApiKey, + Some(&first.reference), + "x-api-key", + "second-secret", + ) + .await + .unwrap(); + assert_ne!( + first.context.credential_identity, + second.context.credential_identity + ); + assert!(authority.lease(&lease).await.is_err()); + let mut fresh = lease; + fresh.context = second.context; + fresh.reference = second.reference; + assert_eq!( + authority.lease(&fresh).await.unwrap().value, + "second-secret" + ); + let mut wrong = fresh.clone(); + wrong.context.config_revision += 1; + assert!(authority.lease(&wrong).await.is_err()); + wrong = fresh.clone(); + wrong.worker.boot_id = "old-boot".into(); + assert!(authority.lease(&wrong).await.is_err()); + // A new boot fences old intents, but preserves durable credential authority. + worker.0.lock().unwrap().boot_id = "boot-b".into(); + assert!(authority.lease(&fresh).await.is_err()); + fresh.worker.boot_id = "boot-b".into(); + assert_eq!( + authority.lease(&fresh).await.unwrap().value, + "second-secret" + ); + let current = authority + .status("connection-a", &fresh.reference) + .await + .unwrap(); + assert!(authority.revoke(&first).await.is_err()); + let revoked = authority.revoke(¤t).await.unwrap(); + assert_eq!(revoked.status, "revoked"); + assert!(authority.lease(&fresh).await.is_err()); + clock.advance(std::time::Duration::from_secs(10)); + assert!(authority.lease(&fresh).await.is_err()); + } +} diff --git a/crates/engram-coordinator/tests/mcp_control.rs b/crates/engram-coordinator/tests/mcp_control.rs new file mode 100644 index 000000000..3d917bf7d --- /dev/null +++ b/crates/engram-coordinator/tests/mcp_control.rs @@ -0,0 +1,171 @@ +use engram_coordinator::{ + mcp::{control::*, model::Registry, *}, + oauth::OAuthManager, +}; +use engram_core::traits::StaticSecretStore; +use engram_crypto::EnvVarKeyProvider; +use engram_protocol::mcp as wire; +use engram_sim::{ManualClock, SimEntropy, SimMetadataStore}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::Arc, +}; +use tokio::io::{AsyncBufReadExt, BufReader}; + +#[tokio::test] +async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() { + let mut fixture = tokio::process::Command::new("node") + .arg(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/mcp-worker.mjs" + )) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .kill_on_drop(true) + .spawn() + .unwrap(); + let mut line = String::new(); + BufReader::new(fixture.stdout.take().unwrap()) + .read_line(&mut line) + .await + .unwrap(); + let f: serde_json::Value = serde_json::from_str(&line).unwrap(); + let clock = ManualClock::new(); + let entropy = Arc::new(SimEntropy::seeded(1)); + let meta = SimMetadataStore::new(clock.clone(), entropy.clone()); + let manager = OAuthManager::new( + meta, + Arc::new(EnvVarKeyProvider::from_bytes([7; 32], "test")), + clock, + entropy, + Arc::new(StaticSecretStore::new(HashMap::new())), + ); + let (_, shutdown) = tokio::sync::watch::channel(false); + assert!( + engram_coordinator::mcp::startup::start(None, manager.clone(), shutdown.clone()) + .await + .unwrap() + .is_none() + ); + assert!(engram_coordinator::mcp::startup::start( + Some(std::path::Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../deploy/mcp-control.example.json" + ))), + manager.clone(), + shutdown + ) + .await + .unwrap() + .is_none()); + let authority = Arc::new( + McpAuthority::new( + manager, + Arc::new(SystemSecurityEntropy), + Registry { + deployment_id: "deployment-a".into(), + ..Default::default() + }, + BTreeMap::new(), + ) + .unwrap(), + ); + for pinned in [true, false] { + let tls = ControlTls { + certificate: f["cert"].as_str().unwrap().as_bytes().to_vec(), + private_key: f["key"].as_str().unwrap().as_bytes().to_vec(), + client_ca: f["ca"].as_str().unwrap().as_bytes().to_vec(), + orchestrator_fingerprints: vec![if pinned { + f["fingerprint"].as_str().unwrap().into() + } else { + "00:".repeat(31) + "00" + }], + }; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = listener.local_addr().unwrap(); + let (stop, done) = tokio::sync::oneshot::channel::<()>(); + let task = tokio::spawn( + server(authority.clone(), tls) + .unwrap() + .serve_with_incoming_shutdown( + tonic::transport::server::TcpIncoming::from(listener), + async { + let _ = done.await; + }, + ), + ); + let endpoint = tonic::transport::Endpoint::from_shared(format!( + "https://localhost:{}", + address.port() + )) + .unwrap(); + let tls = tonic::transport::ClientTlsConfig::new() + .ca_certificate(tonic::transport::Certificate::from_pem( + f["ca"].as_str().unwrap(), + )) + .domain_name("localhost"); + let no_cert = endpoint + .clone() + .tls_config(tls.clone()) + .unwrap() + .connect() + .await; + if let Ok(channel) = no_cert { + assert!( + wire::credential_control_client::CredentialControlClient::new(channel) + .get_status(wire::GetStatusRequest::default()) + .await + .is_err() + ); + } + let channel = endpoint + .tls_config(tls.identity(tonic::transport::Identity::from_pem( + f["cert"].as_str().unwrap(), + f["key"].as_str().unwrap(), + ))) + .unwrap() + .connect() + .await + .unwrap(); + let error = wire::credential_control_client::CredentialControlClient::new(channel) + .get_status(wire::GetStatusRequest { + protocol: 2, + ..Default::default() + }) + .await + .unwrap_err(); + assert_eq!( + error.code(), + if pinned { + tonic::Code::InvalidArgument + } else { + tonic::Code::PermissionDenied + } + ); + let plaintext = + tonic::transport::Endpoint::from_shared(format!("http://localhost:{}", address.port())) + .unwrap() + .connect_timeout(std::time::Duration::from_secs(1)) + .connect() + .await; + if let Ok(channel) = plaintext { + assert!( + wire::credential_control_client::CredentialControlClient::new(channel) + .get_status(wire::GetStatusRequest::default()) + .await + .is_err() + ); + } + let _ = stop.send(()); + task.await.unwrap().unwrap(); + } + assert!(!Registry::default().enabled); + assert!(!format!( + "{:?}", + wire::PutSecretRequest { + value: "private-value".into(), + ..Default::default() + } + ) + .contains("private-value")); +} diff --git a/crates/engram-coordinator/tests/mcp_credentials.rs b/crates/engram-coordinator/tests/mcp_credentials.rs index ca75b1b6c..69218a5c3 100644 --- a/crates/engram-coordinator/tests/mcp_credentials.rs +++ b/crates/engram-coordinator/tests/mcp_credentials.rs @@ -269,3 +269,29 @@ mod publication_race { } } } + +#[tokio::test] +async fn generic_connector_resolver_rejects_mcp_before_loading_credentials() { + use engram_coordinator::oauth::{OAuthManager, OAuthServiceError}; + use engram_core::types::oauth::{OAuthCredentialKey, OAuthSubjectKind}; + let clock = ManualClock::new(); + let entropy = Arc::new(SimEntropy::seeded(4)); + let meta = SimMetadataStore::new(clock.clone(), entropy.clone()); + let manager = OAuthManager::new( + meta, + Arc::new(EnvVarKeyProvider::from_bytes([7; 32], "test")), + clock, + entropy, + Arc::new(StaticSecretStore::new(HashMap::new())), + ); + assert!(matches!( + manager + .resolve_connector_token(&OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: "connection-a".into(), + provider: "registered-a".into() + }) + .await, + Err(OAuthServiceError::BadRequest(_)) + )); +} diff --git a/crates/engram-coordinator/tests/mcp_oauth.rs b/crates/engram-coordinator/tests/mcp_oauth.rs new file mode 100644 index 000000000..da25db700 --- /dev/null +++ b/crates/engram-coordinator/tests/mcp_oauth.rs @@ -0,0 +1,598 @@ +use engram_coordinator::{ + mcp::{model::*, oauth::*, worker::*, *}, + oauth::OAuthManager, +}; +use engram_core::{ + traits::{Clock, StaticSecretStore}, + types::oauth::*, +}; +use engram_crypto::EnvVarKeyProvider; +use engram_sim::{ManualClock, SimEntropy, SimMetadataStore}; +use serde_json::{json, Value}; +use std::{ + collections::{BTreeMap, BTreeSet, HashMap}, + sync::{ + atomic::{AtomicU8, Ordering}, + Arc, Mutex, + }, +}; +struct Security(AtomicU8); +impl SecurityEntropy for Security { + fn bytes(&self) -> Result<[u8; 32]> { + Ok([self.0.fetch_add(1, Ordering::SeqCst); 32]) + } +} +struct Provider { + identity: WorkerIdentity, + registration: ClientRegistration, + grants: Mutex>, + calls: Mutex>, +} +#[async_trait::async_trait] +impl WorkerTransport for Provider { + async fn identity(&self) -> Result { + Ok(self.identity.clone()) + } + async fn rpc( + &self, + _: &WorkerIdentity, + intent: &WorkerIntent, + op: &WorkerOperation, + ) -> Result { + let value = serde_json::to_value(op).unwrap(); + assert_eq!(intent.operation_digest, digest(&value).unwrap()); + self.calls.lock().unwrap().push(value); + match op { + WorkerOperation::Metadata { endpoint } + if endpoint == &self.registration.resource_metadata.endpoint => + { + Ok( + json!({"resource":self.registration.resource.endpoint,"authorization_servers":[self.registration.issuer]}), + ) + } + WorkerOperation::Metadata { .. } => Ok( + json!({"issuer":self.registration.issuer,"authorization_endpoint":self.registration.authorization.endpoint,"token_endpoint":self.registration.token.endpoint,"code_challenge_methods_supported":["S256"],"authorization_response_iss_parameter_supported":true}), + ), + WorkerOperation::ValidateDestination { .. } => Ok(json!({"valid":true})), + WorkerOperation::Token { .. } => { + self.grants.lock().unwrap().pop().ok_or(McpError::Transport) + } + } + } +} +struct Fixture { + authority: McpAuthority, + replica: McpAuthority, + clock: Arc, + meta: Arc, + provider: Arc, + change: ChangeContext, +} +fn fixture(grants: Vec) -> Fixture { + fixture_with_client(grants, false) +} +fn fixture_with_client(grants: Vec, confidential: bool) -> Fixture { + let clock = ManualClock::new(); + let entropy = Arc::new(SimEntropy::seeded(9)); + let meta = SimMetadataStore::new(clock.clone(), entropy.clone()); + let manager = OAuthManager::new( + meta.clone(), + Arc::new(EnvVarKeyProvider::from_bytes([8; 32], "test")), + clock.clone(), + entropy, + Arc::new(StaticSecretStore::new(HashMap::new())), + ); + let d = |path: &str| Destination { + zone_id: "public".into(), + endpoint: format!("https://auth.example/{path}"), + }; + let registration = ClientRegistration { + id: "registered-a".into(), + client_id: "public-client".into(), + confidential, + redirect_uri: "https://app.example/callback".into(), + issuer: "https://auth.example/".into(), + resource: d("mcp"), + resource_metadata: d("resource-metadata"), + authorization_metadata: d("metadata"), + authorization: d("authorize"), + token: d("token"), + scopes: BTreeSet::from(["tools.read".into()]), + issuer_parameter_required: false, + account_field: Some("account".into()), + }; + let identity = WorkerIdentity { + protocol: 1, + boot_id: "boot-a".into(), + zone_id: "public".into(), + policy_digest: "a".repeat(64), + policy_revision: "1".into(), + }; + let endpoints = [ + ®istration.resource, + ®istration.resource_metadata, + ®istration.authorization_metadata, + ®istration.authorization, + ®istration.token, + ] + .into_iter() + .map(|d| d.endpoint.clone()) + .collect(); + let registry = Registry { + enabled: true, + deployment_id: "deployment-a".into(), + zones: BTreeMap::from([( + "public".into(), + Zone { + policy_digest: identity.policy_digest.clone(), + policy_revision: "1".into(), + endpoints, + }, + )]), + registrations: BTreeMap::from([(registration.id.clone(), registration.clone())]), + }; + let provider = Arc::new(Provider { + identity, + registration: registration.clone(), + grants: Mutex::new(grants), + calls: Mutex::new(vec![]), + }); + let security = Arc::new(Security(AtomicU8::new(1))); + let workers = BTreeMap::from([( + "public".into(), + provider.clone() as Arc, + )]); + let authority = McpAuthority::new( + manager.clone(), + security.clone(), + registry.clone(), + workers.clone(), + ) + .unwrap(); + let replica = McpAuthority::new(manager, security, registry, workers).unwrap(); + Fixture { + authority, + replica, + clock, + meta, + provider, + change: ChangeContext { + connection_id: "connection-a".into(), + config_revision: 1, + previous_authorization_generation: 0, + resource: registration.resource, + }, + } +} +fn grant(token: &str) -> Value { + json!({"access_token":token,"token_type":"Bearer","expires_in":60,"scope":"tools.read","refresh_token":"rotating-refresh"}) +} +async fn begin(f: &Fixture) -> BegunAuthorization { + f.authority + .begin(&f.change, "registered-a", None, None, "actor-a", true) + .await + .unwrap() +} +fn callback(b: &BegunAuthorization) -> Callback { + let u = url::Url::parse(&b.authorization_url).unwrap(); + Callback { + context: b.context.clone(), + registration_id: "registered-a".into(), + flow_id: b.flow_id, + state: u + .query_pairs() + .find(|(k, _)| k == "state") + .unwrap() + .1 + .into_owned(), + redirect_uri: "https://app.example/callback".into(), + issuer: Some("https://auth.example/".into()), + code: "authorization-code".into(), + actor_id: "actor-a".into(), + } +} +fn lease(f: &Fixture, status: &CredentialStatus) -> ResourceLease { + let now = f.clock.now_utc().timestamp_millis(); + ResourceLease { + context: status.context.clone(), + reference: status.reference.clone(), + worker: f.provider.identity.clone(), + intent_id: "intent-a".into(), + request_digest: "a".repeat(64), + operation: "discover".into(), + scope: WorkerScope::Administration { + request_id: "request-a".into(), + actor_id: "actor-a".into(), + }, + expires_at: now + 10000, + admission: WorkerLease { + id: "lease-a".into(), + deployment_id: "deployment-a".into(), + expires_at: now + 10000, + }, + } +} +#[tokio::test] +async fn opaque_authorization_consumes_before_exchange_and_refresh_preserves_identity() { + let f = fixture(vec![grant("new-token"), grant("opaque-token")]); + let begun = begin(&f).await; + let cb = callback(&begun); + let (a, b) = tokio::join!(f.authority.callback(&cb), f.replica.callback(&cb)); + assert_eq!(usize::from(a.is_ok()) + usize::from(b.is_ok()), 1); + let status = a.or(b).unwrap(); + let first = lease(&f, &status); + assert_eq!( + f.authority.lease(&first).await.unwrap().value, + "Bearer opaque-token" + ); + f.clock.advance(std::time::Duration::from_secs(61)); + let next = lease(&f, &status); + assert_eq!( + f.replica.lease(&next).await.unwrap().value, + "Bearer new-token" + ); + assert_eq!(status.context, begun.context); + let calls = f.provider.calls.lock().unwrap(); + let tokens: Vec<_> = calls.iter().filter(|v| v["kind"] == "token").collect(); + assert_eq!(tokens.len(), 2); + assert!(tokens + .iter() + .all(|v| v["grant"].get("client_secret").is_none())); + assert_eq!(tokens[0]["grant"]["resource"], "https://auth.example/mcp"); + assert_eq!(calls.len(), 5, "no user-info or JWT probe"); +} +#[tokio::test] +async fn callback_rejects_state_issuer_redirect_expiry_and_scope() { + for defect in ["state", "issuer", "redirect", "expiry", "scope", "type"] { + let mut response = grant("opaque"); + if defect == "scope" { + response["scope"] = json!("tools.write"); + } + if defect == "type" { + response.as_object_mut().unwrap().remove("token_type"); + } + let f = fixture(vec![response]); + let b = begin(&f).await; + let mut cb = callback(&b); + match defect { + "state" => cb.state = "wrong".into(), + "issuer" => cb.issuer = None, + "redirect" => cb.redirect_uri = "https://app.example/other".into(), + "expiry" => f.clock.advance(std::time::Duration::from_secs(600)), + _ => {} + } + assert!(f.authority.callback(&cb).await.is_err(), "{defect}"); + use engram_core::traits::MetadataStore; + assert!(f + .meta + .get_oauth_credential(&OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: "connection-a".into(), + provider: "registered-a".into() + }) + .await + .unwrap() + .is_none()); + } +} +#[tokio::test] +async fn uncertain_refresh_cannot_reuse_rotated_token_after_claim_expiry() { + let f = fixture(vec![grant("opaque")]); + let b = begin(&f).await; + let status = f.authority.callback(&callback(&b)).await.unwrap(); + f.clock.advance(std::time::Duration::from_secs(61)); + assert!(f.authority.lease(&lease(&f, &status)).await.is_err()); + f.clock.advance(std::time::Duration::from_secs(31)); + assert!(f.authority.lease(&lease(&f, &status)).await.is_err()); + assert_eq!( + f.provider + .calls + .lock() + .unwrap() + .iter() + .filter(|v| v["kind"] == "token") + .count(), + 2 + ); +} + +#[tokio::test] +async fn reconnect_fences_old_binding_before_publication_and_revoke_is_cas() { + let f = fixture(vec![grant("replacement"), grant("original")]); + let first = f + .authority + .callback(&callback(&begin(&f).await)) + .await + .unwrap(); + let old_lease = lease(&f, &first); + let mut change = f.change.clone(); + change.previous_authorization_generation = first.context.authorization_generation; + let next = f + .authority + .begin( + &change, + "registered-a", + None, + Some(first.version), + "actor-a", + true, + ) + .await + .unwrap(); + let second = f.replica.callback(&callback(&next)).await.unwrap(); + assert_ne!( + first.context.credential_identity, + second.context.credential_identity + ); + assert_eq!( + second.context.authorization_generation, + first.context.authorization_generation + 1 + ); + assert!(f.authority.lease(&old_lease).await.is_err()); + assert_eq!( + f.authority.lease(&lease(&f, &second)).await.unwrap().value, + "Bearer replacement" + ); + assert!(f.authority.revoke(&first).await.is_err()); + assert_eq!(f.authority.revoke(&second).await.unwrap().status, "revoked"); + assert!(f.authority.lease(&lease(&f, &second)).await.is_err()); +} +#[tokio::test] +async fn provider_account_change_and_scope_expansion_fail_refresh() { + for field in ["account", "scope", "resource", "issuer"] { + let mut first = grant("original"); + first["account"] = json!("account-a"); + let mut refresh = grant("replacement"); + refresh[field] = json!("changed"); + let f = fixture(vec![refresh, first]); + let status = f + .authority + .callback(&callback(&begin(&f).await)) + .await + .unwrap(); + f.clock.advance(std::time::Duration::from_secs(61)); + assert!( + f.authority.lease(&lease(&f, &status)).await.is_err(), + "{field}" + ); + } +} +#[tokio::test] +async fn confidential_secret_rotation_fences_oauth_even_without_orchestrator_update() { + use engram_core::types::org_secret::McpSecretRole; + let f = fixture_with_client(vec![grant("token")], true); + let secret = f + .authority + .put_secret( + &f.change, + McpSecretRole::ClientSecret, + None, + "authorization", + "client-private", + ) + .await + .unwrap(); + let b = f + .authority + .begin( + &f.change, + "registered-a", + Some(&secret.reference), + None, + "actor-a", + true, + ) + .await + .unwrap(); + let status = f.authority.callback(&callback(&b)).await.unwrap(); + { + let calls = f.provider.calls.lock().unwrap(); + let token = calls.iter().find(|v| v["kind"] == "token").unwrap(); + assert_eq!(token["endpoint"], "https://auth.example/token"); + assert_eq!(token["grant"]["client_secret"], "client-private"); + } + let mut change = f.change.clone(); + change.previous_authorization_generation = secret.context.authorization_generation; + f.authority + .put_secret( + &change, + McpSecretRole::ClientSecret, + Some(&secret.reference), + "authorization", + "rotated-private", + ) + .await + .unwrap(); + assert!(f.authority.lease(&lease(&f, &status)).await.is_err()); +} +#[tokio::test] +async fn encrypted_flow_swap_cannot_exchange_or_publish() { + let f = fixture(vec![grant("token")]); + let first = begin(&f).await; + let mut other = f.change.clone(); + other.connection_id = "connection-b".into(); + let second = f + .authority + .begin(&other, "registered-a", None, None, "actor-a", true) + .await + .unwrap(); + f.meta.with_db_mut(|db| { + let a = db.mcp_flow_states.remove(&first.flow_id).unwrap(); + let b = db.mcp_flow_states.remove(&second.flow_id).unwrap(); + db.mcp_flow_states.insert(first.flow_id, b); + db.mcp_flow_states.insert(second.flow_id, a); + }); + assert!(f.replica.callback(&callback(&first)).await.is_err()); + assert!(f.replica.callback(&callback(&second)).await.is_err()); + assert!(f + .provider + .calls + .lock() + .unwrap() + .iter() + .all(|v| v["kind"] != "token")); +} + +#[tokio::test] +async fn saturated_oauth_budget_does_not_claim_or_break_an_undispatched_refresh() { + use engram_core::{traits::MetadataStore, types::mcp::McpOAuthAdmission}; + let f = fixture(vec![grant("new"), grant("old")]); + let status = f + .authority + .callback(&callback(&begin(&f).await)) + .await + .unwrap(); + f.clock.advance(std::time::Duration::from_secs(61)); + let admissions: Vec<_> = (100..104) + .map(|n| McpOAuthAdmission { + id: uuid::Uuid::from_u128(n), + deployment_id: "deployment-a".into(), + connection_id: format!("busy-{n}"), + expires_at: f.clock.now_utc() + chrono::Duration::seconds(20), + }) + .collect(); + for a in &admissions { + assert!(f.meta.acquire_mcp_oauth_admission(a).await.unwrap()); + } + assert!(f.authority.lease(&lease(&f, &status)).await.is_err()); + let row = f + .meta + .get_oauth_credential(&OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: "connection-a".into(), + provider: "registered-a".into(), + }) + .await + .unwrap() + .unwrap(); + assert!( + row.broken_at.is_none(), + "admission denial must not consume the refresh claim" + ); + f.meta + .release_mcp_oauth_admission(&admissions[0]) + .await + .unwrap(); + assert_eq!( + f.authority.lease(&lease(&f, &status)).await.unwrap().value, + "Bearer new" + ); +} + +#[tokio::test] +async fn revoked_client_secret_cannot_start_a_new_authorization() { + use engram_core::types::org_secret::McpSecretRole; + let f = fixture_with_client(vec![grant("token")], true); + let secret = f + .authority + .put_secret( + &f.change, + McpSecretRole::ClientSecret, + None, + "authorization", + "client-private", + ) + .await + .unwrap(); + let revoked = f.authority.revoke(&secret).await.unwrap(); + let mut change = f.change.clone(); + change.previous_authorization_generation = revoked.context.authorization_generation; + assert!(f + .authority + .begin( + &change, + "registered-a", + Some(&revoked.reference), + None, + "actor-a", + true + ) + .await + .is_err()); + assert!(f.provider.calls.lock().unwrap().is_empty()); +} + +#[tokio::test] +async fn revoke_before_authority_switch_fences_old_leases_and_late_callbacks() { + use engram_core::types::org_secret::McpSecretRole; + let f = fixture(vec![ + grant("replacement"), + grant("late"), + grant("other"), + grant("original"), + ]); + let original = f + .authority + .callback(&callback(&begin(&f).await)) + .await + .unwrap(); + let original_lease = lease(&f, &original); + let mut other_change = f.change.clone(); + other_change.connection_id = "connection-b".into(); + let other_flow = f + .authority + .begin(&other_change, "registered-a", None, None, "actor-a", true) + .await + .unwrap(); + let other = f.authority.callback(&callback(&other_flow)).await.unwrap(); + let mut change = f.change.clone(); + change.previous_authorization_generation = original.context.authorization_generation; + let late = f + .authority + .begin( + &change, + "registered-a", + None, + Some(original.version), + "actor-a", + true, + ) + .await + .unwrap(); + let revoked = f.authority.revoke(&original).await.unwrap(); + assert_eq!(revoked.status, "revoked"); + assert_eq!(revoked.version, original.version + 1); + let replacement = f + .authority + .put_secret( + &change, + McpSecretRole::ApiKey, + None, + "x-api-key", + "replacement-key", + ) + .await + .unwrap(); + // No orchestrator publication occurs: the old frozen lease still fails. + assert!(f.authority.lease(&original_lease).await.is_err()); + assert!(f.replica.callback(&callback(&late)).await.is_err()); + assert_eq!( + f.authority + .status("connection-a", &original.reference) + .await + .unwrap() + .status, + "revoked" + ); + assert_eq!( + f.authority.lease(&lease(&f, &other)).await.unwrap().value, + "Bearer other" + ); + let old_key_lease = lease(&f, &replacement); + let key_receipt = f.authority.revoke(&replacement).await.unwrap(); + change.previous_authorization_generation = key_receipt.context.authorization_generation; + let flow = f + .authority + .begin( + &change, + "registered-a", + None, + Some(revoked.version), + "actor-a", + true, + ) + .await + .unwrap(); + f.authority.callback(&callback(&flow)).await.unwrap(); + assert!(f.authority.lease(&old_key_lease).await.is_err()); +} diff --git a/crates/engram-crypto/src/lib.rs b/crates/engram-crypto/src/lib.rs index d86c6c50e..3b7fa548a 100644 --- a/crates/engram-crypto/src/lib.rs +++ b/crates/engram-crypto/src/lib.rs @@ -284,3 +284,12 @@ mod tests { )); } } + +/// Security tokens use OS entropy, never the reproducible simulation RNG. +pub fn security_random_bytes() -> Result<[u8; 32], CryptoError> { + let mut bytes = [0; 32]; + SysRng + .try_fill_bytes(&mut bytes) + .map_err(|_| CryptoError::Provider("security entropy unavailable".into()))?; + Ok(bytes) +} diff --git a/crates/engram-protocol/build.rs b/crates/engram-protocol/build.rs index 806716964..11e8704d2 100644 --- a/crates/engram-protocol/build.rs +++ b/crates/engram-protocol/build.rs @@ -47,5 +47,15 @@ fn main() -> Result<(), Box> { // reach 16 MiB at 1M IDs; the host server bumps its decode // cap inline at the call site rather than globally here. .compile_protos(&protos, &includes)?; + tonic_prost_build::configure() + .build_server(true) + .build_client(true) + .skip_debug([ + ".engram.mcp.v1.PutSecretRequest", + ".engram.mcp.v1.CompleteAuthorizationRequest", + ".engram.mcp.v1.BeginAuthorizationResponse", + ".engram.mcp.v1.LeaseResponse", + ]) + .compile_protos(&["proto/engram/mcp/v1/control.proto"], &includes)?; Ok(()) } diff --git a/crates/engram-protocol/proto/engram/mcp/v1/control.proto b/crates/engram-protocol/proto/engram/mcp/v1/control.proto new file mode 100644 index 000000000..c437111b4 --- /dev/null +++ b/crates/engram-protocol/proto/engram/mcp/v1/control.proto @@ -0,0 +1,144 @@ +syntax = "proto3"; +package engram.mcp.v1; + +// Internal protocol 1. Served only on the separate mTLS listener. +service CredentialControl { + rpc PutSecret(PutSecretRequest) returns (CredentialStatus); + rpc BeginAuthorization(BeginAuthorizationRequest) returns (BeginAuthorizationResponse); + rpc CompleteAuthorization(CompleteAuthorizationRequest) returns (CredentialStatus); + rpc GetStatus(GetStatusRequest) returns (CredentialStatus); + rpc Revoke(RevokeRequest) returns (CredentialStatus); + rpc Lease(LeaseRequest) returns (LeaseResponse); +} +message Destination { + string zone_id = 1; + string endpoint = 2; +} +message ChangeContext { + string connection_id = 1; + int64 config_revision = 2; + int64 previous_authorization_generation = 3; + Destination resource = 4; +} +message AuthorityContext { + string connection_id = 1; + int64 config_revision = 2; + int64 authorization_generation = 3; + string credential_identity = 4; + Destination resource = 5; + string policy_digest = 6; + string policy_revision = 7; +} +enum CredentialKind { + CREDENTIAL_KIND_UNSPECIFIED = 0; + CREDENTIAL_KIND_API_KEY = 1; + CREDENTIAL_KIND_CLIENT_SECRET = 2; + CREDENTIAL_KIND_OAUTH = 3; +} +message CredentialReference { + CredentialKind kind = 1; + string name = 2; + string connection_id = 3; + int64 generation = 4; + string registration_id = 5; +} +message CredentialStatus { + uint32 protocol = 1; + AuthorityContext context = 2; + CredentialReference reference = 3; + int64 version = 4; + string status = 5; +} +message PutSecretRequest { + uint32 protocol = 1; + ChangeContext context = 2; + CredentialKind kind = 3; + CredentialReference expected = 4; + string header = 5; + string value = 6; + string actor_id = 7; +} +message BeginAuthorizationRequest { + uint32 protocol = 1; + ChangeContext context = 2; + string registration_id = 3; + CredentialReference client_secret = 4; + optional int64 expected_version = 5; + string actor_id = 6; + bool sharing_consent = 7; +} +message BeginAuthorizationResponse { + uint32 protocol = 1; + string flow_id = 2; + AuthorityContext context = 3; + string authorization_url = 4; + int64 expires_at = 5; +} +message CompleteAuthorizationRequest { + uint32 protocol = 1; + AuthorityContext context = 2; + string registration_id = 3; + string flow_id = 4; + string state = 5; + string redirect_uri = 6; + optional string issuer = 7; + string code = 8; + string actor_id = 9; +} +message GetStatusRequest { + uint32 protocol = 1; + string connection_id = 2; + CredentialReference reference = 3; +} +message RevokeRequest { + uint32 protocol = 1; + CredentialStatus expected = 2; + string actor_id = 3; +} +message WorkerIdentity { + uint32 protocol = 1; + string boot_id = 2; + string zone_id = 3; + string policy_digest = 4; + string policy_revision = 5; +} +message Admission { + string id = 1; + string deployment_id = 2; + int64 expires_at = 3; +} +message AdministrationScope { + string request_id = 1; + string actor_id = 2; +} +message ExecutionScope { + string session_id = 1; + string run_id = 2; + string tool_call_id = 3; + string binding_hash = 4; +} +message LeaseRequest { + uint32 protocol = 1; + AuthorityContext context = 2; + CredentialReference reference = 3; + WorkerIdentity worker = 4; + string intent_id = 5; + string request_digest = 6; + string operation = 7; + int64 expires_at = 8; + Admission admission = 9; + oneof scope { + AdministrationScope administration = 10; + ExecutionScope execution = 11; + } +} + +// Transient operation memory only. Never queue, cache, persist, log, or return +// this message to a browser or guest. Worker dispatch consumes the bound intent. +message LeaseResponse { + uint32 protocol = 1; + string header = 2; + string value = 3; + int64 expires_at = 4; + string intent_id = 5; +} diff --git a/crates/engram-protocol/src/lib.rs b/crates/engram-protocol/src/lib.rs index c360e44d9..7efaf4cd6 100644 --- a/crates/engram-protocol/src/lib.rs +++ b/crates/engram-protocol/src/lib.rs @@ -25,3 +25,5 @@ pub mod wire; pub use admin::HostAdminHandler; pub use heartbeat::*; pub use wire::{WireExecRequest, WireReapStats, WIRE_VERSION}; + +pub mod mcp; diff --git a/crates/engram-protocol/src/mcp.rs b/crates/engram-protocol/src/mcp.rs new file mode 100644 index 000000000..619f4d3b2 --- /dev/null +++ b/crates/engram-protocol/src/mcp.rs @@ -0,0 +1,14 @@ +//! Internal MCP credential control protocol 1; no cleartext app registration. +tonic::include_proto!("engram.mcp.v1"); +pub const PROTOCOL: u32 = 1; +macro_rules! redacted_debug { + ($($ty:ty),*) => {$ (impl std::fmt::Debug for $ty { + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result {f.write_str("McpControlMessage([redacted])")} + })*}; +} +redacted_debug!( + PutSecretRequest, + CompleteAuthorizationRequest, + BeginAuthorizationResponse, + LeaseResponse +); diff --git a/deploy/mcp-control.example.json b/deploy/mcp-control.example.json new file mode 100644 index 000000000..f982d6261 --- /dev/null +++ b/deploy/mcp-control.example.json @@ -0,0 +1,65 @@ +{ + "registry": { + "enabled": false, + "deploymentId": "deployment-a", + "zones": { + "public": { + "policyDigest": "0000000000000000000000000000000000000000000000000000000000000000", + "policyRevision": "1", + "endpoints": [ + "https://tools.example/mcp", + "https://tools.example/.well-known/oauth-protected-resource", + "https://auth.example/.well-known/oauth-authorization-server", + "https://auth.example/authorize", + "https://auth.example/token" + ] + } + }, + "registrations": { + "registered-public": { + "id": "registered-public", + "clientId": "registered-client-id", + "confidential": false, + "redirectUri": "https://app.example/mcp/callback", + "issuer": "https://auth.example/", + "resource": { + "zoneId": "public", + "endpoint": "https://tools.example/mcp" + }, + "resourceMetadata": { + "zoneId": "public", + "endpoint": "https://tools.example/.well-known/oauth-protected-resource" + }, + "authorizationMetadata": { + "zoneId": "public", + "endpoint": "https://auth.example/.well-known/oauth-authorization-server" + }, + "authorization": { + "zoneId": "public", + "endpoint": "https://auth.example/authorize" + }, + "token": { + "zoneId": "public", + "endpoint": "https://auth.example/token" + }, + "scopes": ["tools.read"], + "issuerParameterRequired": true, + "accountField": null + } + } + }, + "listenAddr": "0.0.0.0:50062", + "serverCertificateFile": "/run/mcp-control/server.crt", + "serverPrivateKeyFile": "/run/mcp-control/server.key", + "orchestratorCaFile": "/run/mcp-control/orchestrator-ca.crt", + "orchestratorFingerprints": [ + "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" + ], + "workers": { + "public": { + "url": "https://mcp-public.example:8443", + "serverCaFile": "/run/mcp-worker/server-ca.crt", + "clientIdentityFile": "/run/mcp-worker/coordinator.pem" + } + } +} diff --git a/orchestrator/src/gen/engram/mcp/v1/control_pb.ts b/orchestrator/src/gen/engram/mcp/v1/control_pb.ts new file mode 100644 index 000000000..57b3404ce --- /dev/null +++ b/orchestrator/src/gen/engram/mcp/v1/control_pb.ts @@ -0,0 +1,751 @@ +// @generated by protoc-gen-es v2.2.5 with parameter "target=ts" +// @generated from file engram/mcp/v1/control.proto (package engram.mcp.v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file engram/mcp/v1/control.proto. + */ +export const file_engram_mcp_v1_control: GenFile = /*@__PURE__*/ + fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJIrIDCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUiZwoNTGVhc2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZoZWFkZXIYAiABKAkSDQoFdmFsdWUYAyABKAkSEgoKZXhwaXJlc19hdBgEIAEoAxIRCglpbnRlbnRfaWQYBSABKAkqjAEKDkNyZWRlbnRpYWxLaW5kEh8KG0NSRURFTlRJQUxfS0lORF9VTlNQRUNJRklFRBAAEhsKF0NSRURFTlRJQUxfS0lORF9BUElfS0VZEAESIQodQ1JFREVOVElBTF9LSU5EX0NMSUVOVF9TRUNSRVQQAhIZChVDUkVERU5USUFMX0tJTkRfT0FVVEgQAzKQBAoRQ3JlZGVudGlhbENvbnRyb2wSTQoJUHV0U2VjcmV0Eh8uZW5ncmFtLm1jcC52MS5QdXRTZWNyZXRSZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEmkKEkJlZ2luQXV0aG9yaXphdGlvbhIoLmVuZ3JhbS5tY3AudjEuQmVnaW5BdXRob3JpemF0aW9uUmVxdWVzdBopLmVuZ3JhbS5tY3AudjEuQmVnaW5BdXRob3JpemF0aW9uUmVzcG9uc2USZQoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEk0KCUdldFN0YXR1cxIfLmVuZ3JhbS5tY3AudjEuR2V0U3RhdHVzUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJHCgZSZXZva2USHC5lbmdyYW0ubWNwLnYxLlJldm9rZVJlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); + +/** + * @generated from message engram.mcp.v1.Destination + */ +export type Destination = Message<"engram.mcp.v1.Destination"> & { + /** + * @generated from field: string zone_id = 1; + */ + zoneId: string; + + /** + * @generated from field: string endpoint = 2; + */ + endpoint: string; +}; + +/** + * Describes the message engram.mcp.v1.Destination. + * Use `create(DestinationSchema)` to create a new message. + */ +export const DestinationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 0); + +/** + * @generated from message engram.mcp.v1.ChangeContext + */ +export type ChangeContext = Message<"engram.mcp.v1.ChangeContext"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: int64 config_revision = 2; + */ + configRevision: bigint; + + /** + * @generated from field: int64 previous_authorization_generation = 3; + */ + previousAuthorizationGeneration: bigint; + + /** + * @generated from field: engram.mcp.v1.Destination resource = 4; + */ + resource?: Destination; +}; + +/** + * Describes the message engram.mcp.v1.ChangeContext. + * Use `create(ChangeContextSchema)` to create a new message. + */ +export const ChangeContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 1); + +/** + * @generated from message engram.mcp.v1.AuthorityContext + */ +export type AuthorityContext = Message<"engram.mcp.v1.AuthorityContext"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: int64 config_revision = 2; + */ + configRevision: bigint; + + /** + * @generated from field: int64 authorization_generation = 3; + */ + authorizationGeneration: bigint; + + /** + * @generated from field: string credential_identity = 4; + */ + credentialIdentity: string; + + /** + * @generated from field: engram.mcp.v1.Destination resource = 5; + */ + resource?: Destination; + + /** + * @generated from field: string policy_digest = 6; + */ + policyDigest: string; + + /** + * @generated from field: string policy_revision = 7; + */ + policyRevision: string; +}; + +/** + * Describes the message engram.mcp.v1.AuthorityContext. + * Use `create(AuthorityContextSchema)` to create a new message. + */ +export const AuthorityContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 2); + +/** + * @generated from message engram.mcp.v1.CredentialReference + */ +export type CredentialReference = Message<"engram.mcp.v1.CredentialReference"> & { + /** + * @generated from field: engram.mcp.v1.CredentialKind kind = 1; + */ + kind: CredentialKind; + + /** + * @generated from field: string name = 2; + */ + name: string; + + /** + * @generated from field: string connection_id = 3; + */ + connectionId: string; + + /** + * @generated from field: int64 generation = 4; + */ + generation: bigint; + + /** + * @generated from field: string registration_id = 5; + */ + registrationId: string; +}; + +/** + * Describes the message engram.mcp.v1.CredentialReference. + * Use `create(CredentialReferenceSchema)` to create a new message. + */ +export const CredentialReferenceSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 3); + +/** + * @generated from message engram.mcp.v1.CredentialStatus + */ +export type CredentialStatus = Message<"engram.mcp.v1.CredentialStatus"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.AuthorityContext context = 2; + */ + context?: AuthorityContext; + + /** + * @generated from field: engram.mcp.v1.CredentialReference reference = 3; + */ + reference?: CredentialReference; + + /** + * @generated from field: int64 version = 4; + */ + version: bigint; + + /** + * @generated from field: string status = 5; + */ + status: string; +}; + +/** + * Describes the message engram.mcp.v1.CredentialStatus. + * Use `create(CredentialStatusSchema)` to create a new message. + */ +export const CredentialStatusSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 4); + +/** + * @generated from message engram.mcp.v1.PutSecretRequest + */ +export type PutSecretRequest = Message<"engram.mcp.v1.PutSecretRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ChangeContext context = 2; + */ + context?: ChangeContext; + + /** + * @generated from field: engram.mcp.v1.CredentialKind kind = 3; + */ + kind: CredentialKind; + + /** + * @generated from field: engram.mcp.v1.CredentialReference expected = 4; + */ + expected?: CredentialReference; + + /** + * @generated from field: string header = 5; + */ + header: string; + + /** + * @generated from field: string value = 6; + */ + value: string; + + /** + * @generated from field: string actor_id = 7; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.PutSecretRequest. + * Use `create(PutSecretRequestSchema)` to create a new message. + */ +export const PutSecretRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 5); + +/** + * @generated from message engram.mcp.v1.BeginAuthorizationRequest + */ +export type BeginAuthorizationRequest = Message<"engram.mcp.v1.BeginAuthorizationRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ChangeContext context = 2; + */ + context?: ChangeContext; + + /** + * @generated from field: string registration_id = 3; + */ + registrationId: string; + + /** + * @generated from field: engram.mcp.v1.CredentialReference client_secret = 4; + */ + clientSecret?: CredentialReference; + + /** + * @generated from field: optional int64 expected_version = 5; + */ + expectedVersion?: bigint; + + /** + * @generated from field: string actor_id = 6; + */ + actorId: string; + + /** + * @generated from field: bool sharing_consent = 7; + */ + sharingConsent: boolean; +}; + +/** + * Describes the message engram.mcp.v1.BeginAuthorizationRequest. + * Use `create(BeginAuthorizationRequestSchema)` to create a new message. + */ +export const BeginAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 6); + +/** + * @generated from message engram.mcp.v1.BeginAuthorizationResponse + */ +export type BeginAuthorizationResponse = Message<"engram.mcp.v1.BeginAuthorizationResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string flow_id = 2; + */ + flowId: string; + + /** + * @generated from field: engram.mcp.v1.AuthorityContext context = 3; + */ + context?: AuthorityContext; + + /** + * @generated from field: string authorization_url = 4; + */ + authorizationUrl: string; + + /** + * @generated from field: int64 expires_at = 5; + */ + expiresAt: bigint; +}; + +/** + * Describes the message engram.mcp.v1.BeginAuthorizationResponse. + * Use `create(BeginAuthorizationResponseSchema)` to create a new message. + */ +export const BeginAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 7); + +/** + * @generated from message engram.mcp.v1.CompleteAuthorizationRequest + */ +export type CompleteAuthorizationRequest = Message<"engram.mcp.v1.CompleteAuthorizationRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.AuthorityContext context = 2; + */ + context?: AuthorityContext; + + /** + * @generated from field: string registration_id = 3; + */ + registrationId: string; + + /** + * @generated from field: string flow_id = 4; + */ + flowId: string; + + /** + * @generated from field: string state = 5; + */ + state: string; + + /** + * @generated from field: string redirect_uri = 6; + */ + redirectUri: string; + + /** + * @generated from field: optional string issuer = 7; + */ + issuer?: string; + + /** + * @generated from field: string code = 8; + */ + code: string; + + /** + * @generated from field: string actor_id = 9; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.CompleteAuthorizationRequest. + * Use `create(CompleteAuthorizationRequestSchema)` to create a new message. + */ +export const CompleteAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 8); + +/** + * @generated from message engram.mcp.v1.GetStatusRequest + */ +export type GetStatusRequest = Message<"engram.mcp.v1.GetStatusRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string connection_id = 2; + */ + connectionId: string; + + /** + * @generated from field: engram.mcp.v1.CredentialReference reference = 3; + */ + reference?: CredentialReference; +}; + +/** + * Describes the message engram.mcp.v1.GetStatusRequest. + * Use `create(GetStatusRequestSchema)` to create a new message. + */ +export const GetStatusRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 9); + +/** + * @generated from message engram.mcp.v1.RevokeRequest + */ +export type RevokeRequest = Message<"engram.mcp.v1.RevokeRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.CredentialStatus expected = 2; + */ + expected?: CredentialStatus; + + /** + * @generated from field: string actor_id = 3; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.RevokeRequest. + * Use `create(RevokeRequestSchema)` to create a new message. + */ +export const RevokeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 10); + +/** + * @generated from message engram.mcp.v1.WorkerIdentity + */ +export type WorkerIdentity = Message<"engram.mcp.v1.WorkerIdentity"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string boot_id = 2; + */ + bootId: string; + + /** + * @generated from field: string zone_id = 3; + */ + zoneId: string; + + /** + * @generated from field: string policy_digest = 4; + */ + policyDigest: string; + + /** + * @generated from field: string policy_revision = 5; + */ + policyRevision: string; +}; + +/** + * Describes the message engram.mcp.v1.WorkerIdentity. + * Use `create(WorkerIdentitySchema)` to create a new message. + */ +export const WorkerIdentitySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 11); + +/** + * @generated from message engram.mcp.v1.Admission + */ +export type Admission = Message<"engram.mcp.v1.Admission"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string deployment_id = 2; + */ + deploymentId: string; + + /** + * @generated from field: int64 expires_at = 3; + */ + expiresAt: bigint; +}; + +/** + * Describes the message engram.mcp.v1.Admission. + * Use `create(AdmissionSchema)` to create a new message. + */ +export const AdmissionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 12); + +/** + * @generated from message engram.mcp.v1.AdministrationScope + */ +export type AdministrationScope = Message<"engram.mcp.v1.AdministrationScope"> & { + /** + * @generated from field: string request_id = 1; + */ + requestId: string; + + /** + * @generated from field: string actor_id = 2; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.AdministrationScope. + * Use `create(AdministrationScopeSchema)` to create a new message. + */ +export const AdministrationScopeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 13); + +/** + * @generated from message engram.mcp.v1.ExecutionScope + */ +export type ExecutionScope = Message<"engram.mcp.v1.ExecutionScope"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 2; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 3; + */ + toolCallId: string; + + /** + * @generated from field: string binding_hash = 4; + */ + bindingHash: string; +}; + +/** + * Describes the message engram.mcp.v1.ExecutionScope. + * Use `create(ExecutionScopeSchema)` to create a new message. + */ +export const ExecutionScopeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 14); + +/** + * @generated from message engram.mcp.v1.LeaseRequest + */ +export type LeaseRequest = Message<"engram.mcp.v1.LeaseRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.AuthorityContext context = 2; + */ + context?: AuthorityContext; + + /** + * @generated from field: engram.mcp.v1.CredentialReference reference = 3; + */ + reference?: CredentialReference; + + /** + * @generated from field: engram.mcp.v1.WorkerIdentity worker = 4; + */ + worker?: WorkerIdentity; + + /** + * @generated from field: string intent_id = 5; + */ + intentId: string; + + /** + * @generated from field: string request_digest = 6; + */ + requestDigest: string; + + /** + * @generated from field: string operation = 7; + */ + operation: string; + + /** + * @generated from field: int64 expires_at = 8; + */ + expiresAt: bigint; + + /** + * @generated from field: engram.mcp.v1.Admission admission = 9; + */ + admission?: Admission; + + /** + * @generated from oneof engram.mcp.v1.LeaseRequest.scope + */ + scope: { + /** + * @generated from field: engram.mcp.v1.AdministrationScope administration = 10; + */ + value: AdministrationScope; + case: "administration"; + } | { + /** + * @generated from field: engram.mcp.v1.ExecutionScope execution = 11; + */ + value: ExecutionScope; + case: "execution"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message engram.mcp.v1.LeaseRequest. + * Use `create(LeaseRequestSchema)` to create a new message. + */ +export const LeaseRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 15); + +/** + * Transient operation memory only. Never queue, cache, persist, log, or return + * this message to a browser or guest. Worker dispatch consumes the bound intent. + * + * @generated from message engram.mcp.v1.LeaseResponse + */ +export type LeaseResponse = Message<"engram.mcp.v1.LeaseResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string header = 2; + */ + header: string; + + /** + * @generated from field: string value = 3; + */ + value: string; + + /** + * @generated from field: int64 expires_at = 4; + */ + expiresAt: bigint; + + /** + * @generated from field: string intent_id = 5; + */ + intentId: string; +}; + +/** + * Describes the message engram.mcp.v1.LeaseResponse. + * Use `create(LeaseResponseSchema)` to create a new message. + */ +export const LeaseResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 16); + +/** + * @generated from enum engram.mcp.v1.CredentialKind + */ +export enum CredentialKind { + /** + * @generated from enum value: CREDENTIAL_KIND_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: CREDENTIAL_KIND_API_KEY = 1; + */ + API_KEY = 1, + + /** + * @generated from enum value: CREDENTIAL_KIND_CLIENT_SECRET = 2; + */ + CLIENT_SECRET = 2, + + /** + * @generated from enum value: CREDENTIAL_KIND_OAUTH = 3; + */ + OAUTH = 3, +} + +/** + * Describes the enum engram.mcp.v1.CredentialKind. + */ +export const CredentialKindSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_engram_mcp_v1_control, 0); + +/** + * Internal protocol 1. Served only on the separate mTLS listener. + * + * @generated from service engram.mcp.v1.CredentialControl + */ +export const CredentialControl: GenService<{ + /** + * @generated from rpc engram.mcp.v1.CredentialControl.PutSecret + */ + putSecret: { + methodKind: "unary"; + input: typeof PutSecretRequestSchema; + output: typeof CredentialStatusSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.BeginAuthorization + */ + beginAuthorization: { + methodKind: "unary"; + input: typeof BeginAuthorizationRequestSchema; + output: typeof BeginAuthorizationResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.CompleteAuthorization + */ + completeAuthorization: { + methodKind: "unary"; + input: typeof CompleteAuthorizationRequestSchema; + output: typeof CredentialStatusSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.GetStatus + */ + getStatus: { + methodKind: "unary"; + input: typeof GetStatusRequestSchema; + output: typeof CredentialStatusSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.Revoke + */ + revoke: { + methodKind: "unary"; + input: typeof RevokeRequestSchema; + output: typeof CredentialStatusSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.Lease + */ + lease: { + methodKind: "unary"; + input: typeof LeaseRequestSchema; + output: typeof LeaseResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_engram_mcp_v1_control, 0); + diff --git a/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts b/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts new file mode 100644 index 000000000..1e83c35c7 --- /dev/null +++ b/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts @@ -0,0 +1,35 @@ +// @generated by protoc-gen-connect-query v2.1.0 with parameter "target=ts" +// @generated from file engram/mcp/v1/control.proto (package engram.mcp.v1, syntax proto3) +/* eslint-disable */ + +import { CredentialControl } from "./control_pb"; + +/** + * @generated from rpc engram.mcp.v1.CredentialControl.PutSecret + */ +export const putSecret = CredentialControl.method.putSecret; + +/** + * @generated from rpc engram.mcp.v1.CredentialControl.BeginAuthorization + */ +export const beginAuthorization = CredentialControl.method.beginAuthorization; + +/** + * @generated from rpc engram.mcp.v1.CredentialControl.CompleteAuthorization + */ +export const completeAuthorization = CredentialControl.method.completeAuthorization; + +/** + * @generated from rpc engram.mcp.v1.CredentialControl.GetStatus + */ +export const getStatus = CredentialControl.method.getStatus; + +/** + * @generated from rpc engram.mcp.v1.CredentialControl.Revoke + */ +export const revoke = CredentialControl.method.revoke; + +/** + * @generated from rpc engram.mcp.v1.CredentialControl.Lease + */ +export const lease = CredentialControl.method.lease; diff --git a/web/src/gen/engram/mcp/v1/control_pb.ts b/web/src/gen/engram/mcp/v1/control_pb.ts new file mode 100644 index 000000000..57b3404ce --- /dev/null +++ b/web/src/gen/engram/mcp/v1/control_pb.ts @@ -0,0 +1,751 @@ +// @generated by protoc-gen-es v2.2.5 with parameter "target=ts" +// @generated from file engram/mcp/v1/control.proto (package engram.mcp.v1, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file engram/mcp/v1/control.proto. + */ +export const file_engram_mcp_v1_control: GenFile = /*@__PURE__*/ + fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJIrIDCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUiZwoNTGVhc2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZoZWFkZXIYAiABKAkSDQoFdmFsdWUYAyABKAkSEgoKZXhwaXJlc19hdBgEIAEoAxIRCglpbnRlbnRfaWQYBSABKAkqjAEKDkNyZWRlbnRpYWxLaW5kEh8KG0NSRURFTlRJQUxfS0lORF9VTlNQRUNJRklFRBAAEhsKF0NSRURFTlRJQUxfS0lORF9BUElfS0VZEAESIQodQ1JFREVOVElBTF9LSU5EX0NMSUVOVF9TRUNSRVQQAhIZChVDUkVERU5USUFMX0tJTkRfT0FVVEgQAzKQBAoRQ3JlZGVudGlhbENvbnRyb2wSTQoJUHV0U2VjcmV0Eh8uZW5ncmFtLm1jcC52MS5QdXRTZWNyZXRSZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEmkKEkJlZ2luQXV0aG9yaXphdGlvbhIoLmVuZ3JhbS5tY3AudjEuQmVnaW5BdXRob3JpemF0aW9uUmVxdWVzdBopLmVuZ3JhbS5tY3AudjEuQmVnaW5BdXRob3JpemF0aW9uUmVzcG9uc2USZQoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEk0KCUdldFN0YXR1cxIfLmVuZ3JhbS5tY3AudjEuR2V0U3RhdHVzUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJHCgZSZXZva2USHC5lbmdyYW0ubWNwLnYxLlJldm9rZVJlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); + +/** + * @generated from message engram.mcp.v1.Destination + */ +export type Destination = Message<"engram.mcp.v1.Destination"> & { + /** + * @generated from field: string zone_id = 1; + */ + zoneId: string; + + /** + * @generated from field: string endpoint = 2; + */ + endpoint: string; +}; + +/** + * Describes the message engram.mcp.v1.Destination. + * Use `create(DestinationSchema)` to create a new message. + */ +export const DestinationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 0); + +/** + * @generated from message engram.mcp.v1.ChangeContext + */ +export type ChangeContext = Message<"engram.mcp.v1.ChangeContext"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: int64 config_revision = 2; + */ + configRevision: bigint; + + /** + * @generated from field: int64 previous_authorization_generation = 3; + */ + previousAuthorizationGeneration: bigint; + + /** + * @generated from field: engram.mcp.v1.Destination resource = 4; + */ + resource?: Destination; +}; + +/** + * Describes the message engram.mcp.v1.ChangeContext. + * Use `create(ChangeContextSchema)` to create a new message. + */ +export const ChangeContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 1); + +/** + * @generated from message engram.mcp.v1.AuthorityContext + */ +export type AuthorityContext = Message<"engram.mcp.v1.AuthorityContext"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: int64 config_revision = 2; + */ + configRevision: bigint; + + /** + * @generated from field: int64 authorization_generation = 3; + */ + authorizationGeneration: bigint; + + /** + * @generated from field: string credential_identity = 4; + */ + credentialIdentity: string; + + /** + * @generated from field: engram.mcp.v1.Destination resource = 5; + */ + resource?: Destination; + + /** + * @generated from field: string policy_digest = 6; + */ + policyDigest: string; + + /** + * @generated from field: string policy_revision = 7; + */ + policyRevision: string; +}; + +/** + * Describes the message engram.mcp.v1.AuthorityContext. + * Use `create(AuthorityContextSchema)` to create a new message. + */ +export const AuthorityContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 2); + +/** + * @generated from message engram.mcp.v1.CredentialReference + */ +export type CredentialReference = Message<"engram.mcp.v1.CredentialReference"> & { + /** + * @generated from field: engram.mcp.v1.CredentialKind kind = 1; + */ + kind: CredentialKind; + + /** + * @generated from field: string name = 2; + */ + name: string; + + /** + * @generated from field: string connection_id = 3; + */ + connectionId: string; + + /** + * @generated from field: int64 generation = 4; + */ + generation: bigint; + + /** + * @generated from field: string registration_id = 5; + */ + registrationId: string; +}; + +/** + * Describes the message engram.mcp.v1.CredentialReference. + * Use `create(CredentialReferenceSchema)` to create a new message. + */ +export const CredentialReferenceSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 3); + +/** + * @generated from message engram.mcp.v1.CredentialStatus + */ +export type CredentialStatus = Message<"engram.mcp.v1.CredentialStatus"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.AuthorityContext context = 2; + */ + context?: AuthorityContext; + + /** + * @generated from field: engram.mcp.v1.CredentialReference reference = 3; + */ + reference?: CredentialReference; + + /** + * @generated from field: int64 version = 4; + */ + version: bigint; + + /** + * @generated from field: string status = 5; + */ + status: string; +}; + +/** + * Describes the message engram.mcp.v1.CredentialStatus. + * Use `create(CredentialStatusSchema)` to create a new message. + */ +export const CredentialStatusSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 4); + +/** + * @generated from message engram.mcp.v1.PutSecretRequest + */ +export type PutSecretRequest = Message<"engram.mcp.v1.PutSecretRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ChangeContext context = 2; + */ + context?: ChangeContext; + + /** + * @generated from field: engram.mcp.v1.CredentialKind kind = 3; + */ + kind: CredentialKind; + + /** + * @generated from field: engram.mcp.v1.CredentialReference expected = 4; + */ + expected?: CredentialReference; + + /** + * @generated from field: string header = 5; + */ + header: string; + + /** + * @generated from field: string value = 6; + */ + value: string; + + /** + * @generated from field: string actor_id = 7; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.PutSecretRequest. + * Use `create(PutSecretRequestSchema)` to create a new message. + */ +export const PutSecretRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 5); + +/** + * @generated from message engram.mcp.v1.BeginAuthorizationRequest + */ +export type BeginAuthorizationRequest = Message<"engram.mcp.v1.BeginAuthorizationRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ChangeContext context = 2; + */ + context?: ChangeContext; + + /** + * @generated from field: string registration_id = 3; + */ + registrationId: string; + + /** + * @generated from field: engram.mcp.v1.CredentialReference client_secret = 4; + */ + clientSecret?: CredentialReference; + + /** + * @generated from field: optional int64 expected_version = 5; + */ + expectedVersion?: bigint; + + /** + * @generated from field: string actor_id = 6; + */ + actorId: string; + + /** + * @generated from field: bool sharing_consent = 7; + */ + sharingConsent: boolean; +}; + +/** + * Describes the message engram.mcp.v1.BeginAuthorizationRequest. + * Use `create(BeginAuthorizationRequestSchema)` to create a new message. + */ +export const BeginAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 6); + +/** + * @generated from message engram.mcp.v1.BeginAuthorizationResponse + */ +export type BeginAuthorizationResponse = Message<"engram.mcp.v1.BeginAuthorizationResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string flow_id = 2; + */ + flowId: string; + + /** + * @generated from field: engram.mcp.v1.AuthorityContext context = 3; + */ + context?: AuthorityContext; + + /** + * @generated from field: string authorization_url = 4; + */ + authorizationUrl: string; + + /** + * @generated from field: int64 expires_at = 5; + */ + expiresAt: bigint; +}; + +/** + * Describes the message engram.mcp.v1.BeginAuthorizationResponse. + * Use `create(BeginAuthorizationResponseSchema)` to create a new message. + */ +export const BeginAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 7); + +/** + * @generated from message engram.mcp.v1.CompleteAuthorizationRequest + */ +export type CompleteAuthorizationRequest = Message<"engram.mcp.v1.CompleteAuthorizationRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.AuthorityContext context = 2; + */ + context?: AuthorityContext; + + /** + * @generated from field: string registration_id = 3; + */ + registrationId: string; + + /** + * @generated from field: string flow_id = 4; + */ + flowId: string; + + /** + * @generated from field: string state = 5; + */ + state: string; + + /** + * @generated from field: string redirect_uri = 6; + */ + redirectUri: string; + + /** + * @generated from field: optional string issuer = 7; + */ + issuer?: string; + + /** + * @generated from field: string code = 8; + */ + code: string; + + /** + * @generated from field: string actor_id = 9; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.CompleteAuthorizationRequest. + * Use `create(CompleteAuthorizationRequestSchema)` to create a new message. + */ +export const CompleteAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 8); + +/** + * @generated from message engram.mcp.v1.GetStatusRequest + */ +export type GetStatusRequest = Message<"engram.mcp.v1.GetStatusRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string connection_id = 2; + */ + connectionId: string; + + /** + * @generated from field: engram.mcp.v1.CredentialReference reference = 3; + */ + reference?: CredentialReference; +}; + +/** + * Describes the message engram.mcp.v1.GetStatusRequest. + * Use `create(GetStatusRequestSchema)` to create a new message. + */ +export const GetStatusRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 9); + +/** + * @generated from message engram.mcp.v1.RevokeRequest + */ +export type RevokeRequest = Message<"engram.mcp.v1.RevokeRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.CredentialStatus expected = 2; + */ + expected?: CredentialStatus; + + /** + * @generated from field: string actor_id = 3; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.RevokeRequest. + * Use `create(RevokeRequestSchema)` to create a new message. + */ +export const RevokeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 10); + +/** + * @generated from message engram.mcp.v1.WorkerIdentity + */ +export type WorkerIdentity = Message<"engram.mcp.v1.WorkerIdentity"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string boot_id = 2; + */ + bootId: string; + + /** + * @generated from field: string zone_id = 3; + */ + zoneId: string; + + /** + * @generated from field: string policy_digest = 4; + */ + policyDigest: string; + + /** + * @generated from field: string policy_revision = 5; + */ + policyRevision: string; +}; + +/** + * Describes the message engram.mcp.v1.WorkerIdentity. + * Use `create(WorkerIdentitySchema)` to create a new message. + */ +export const WorkerIdentitySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 11); + +/** + * @generated from message engram.mcp.v1.Admission + */ +export type Admission = Message<"engram.mcp.v1.Admission"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string deployment_id = 2; + */ + deploymentId: string; + + /** + * @generated from field: int64 expires_at = 3; + */ + expiresAt: bigint; +}; + +/** + * Describes the message engram.mcp.v1.Admission. + * Use `create(AdmissionSchema)` to create a new message. + */ +export const AdmissionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 12); + +/** + * @generated from message engram.mcp.v1.AdministrationScope + */ +export type AdministrationScope = Message<"engram.mcp.v1.AdministrationScope"> & { + /** + * @generated from field: string request_id = 1; + */ + requestId: string; + + /** + * @generated from field: string actor_id = 2; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.AdministrationScope. + * Use `create(AdministrationScopeSchema)` to create a new message. + */ +export const AdministrationScopeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 13); + +/** + * @generated from message engram.mcp.v1.ExecutionScope + */ +export type ExecutionScope = Message<"engram.mcp.v1.ExecutionScope"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 2; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 3; + */ + toolCallId: string; + + /** + * @generated from field: string binding_hash = 4; + */ + bindingHash: string; +}; + +/** + * Describes the message engram.mcp.v1.ExecutionScope. + * Use `create(ExecutionScopeSchema)` to create a new message. + */ +export const ExecutionScopeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 14); + +/** + * @generated from message engram.mcp.v1.LeaseRequest + */ +export type LeaseRequest = Message<"engram.mcp.v1.LeaseRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.AuthorityContext context = 2; + */ + context?: AuthorityContext; + + /** + * @generated from field: engram.mcp.v1.CredentialReference reference = 3; + */ + reference?: CredentialReference; + + /** + * @generated from field: engram.mcp.v1.WorkerIdentity worker = 4; + */ + worker?: WorkerIdentity; + + /** + * @generated from field: string intent_id = 5; + */ + intentId: string; + + /** + * @generated from field: string request_digest = 6; + */ + requestDigest: string; + + /** + * @generated from field: string operation = 7; + */ + operation: string; + + /** + * @generated from field: int64 expires_at = 8; + */ + expiresAt: bigint; + + /** + * @generated from field: engram.mcp.v1.Admission admission = 9; + */ + admission?: Admission; + + /** + * @generated from oneof engram.mcp.v1.LeaseRequest.scope + */ + scope: { + /** + * @generated from field: engram.mcp.v1.AdministrationScope administration = 10; + */ + value: AdministrationScope; + case: "administration"; + } | { + /** + * @generated from field: engram.mcp.v1.ExecutionScope execution = 11; + */ + value: ExecutionScope; + case: "execution"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message engram.mcp.v1.LeaseRequest. + * Use `create(LeaseRequestSchema)` to create a new message. + */ +export const LeaseRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 15); + +/** + * Transient operation memory only. Never queue, cache, persist, log, or return + * this message to a browser or guest. Worker dispatch consumes the bound intent. + * + * @generated from message engram.mcp.v1.LeaseResponse + */ +export type LeaseResponse = Message<"engram.mcp.v1.LeaseResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string header = 2; + */ + header: string; + + /** + * @generated from field: string value = 3; + */ + value: string; + + /** + * @generated from field: int64 expires_at = 4; + */ + expiresAt: bigint; + + /** + * @generated from field: string intent_id = 5; + */ + intentId: string; +}; + +/** + * Describes the message engram.mcp.v1.LeaseResponse. + * Use `create(LeaseResponseSchema)` to create a new message. + */ +export const LeaseResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 16); + +/** + * @generated from enum engram.mcp.v1.CredentialKind + */ +export enum CredentialKind { + /** + * @generated from enum value: CREDENTIAL_KIND_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: CREDENTIAL_KIND_API_KEY = 1; + */ + API_KEY = 1, + + /** + * @generated from enum value: CREDENTIAL_KIND_CLIENT_SECRET = 2; + */ + CLIENT_SECRET = 2, + + /** + * @generated from enum value: CREDENTIAL_KIND_OAUTH = 3; + */ + OAUTH = 3, +} + +/** + * Describes the enum engram.mcp.v1.CredentialKind. + */ +export const CredentialKindSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_engram_mcp_v1_control, 0); + +/** + * Internal protocol 1. Served only on the separate mTLS listener. + * + * @generated from service engram.mcp.v1.CredentialControl + */ +export const CredentialControl: GenService<{ + /** + * @generated from rpc engram.mcp.v1.CredentialControl.PutSecret + */ + putSecret: { + methodKind: "unary"; + input: typeof PutSecretRequestSchema; + output: typeof CredentialStatusSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.BeginAuthorization + */ + beginAuthorization: { + methodKind: "unary"; + input: typeof BeginAuthorizationRequestSchema; + output: typeof BeginAuthorizationResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.CompleteAuthorization + */ + completeAuthorization: { + methodKind: "unary"; + input: typeof CompleteAuthorizationRequestSchema; + output: typeof CredentialStatusSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.GetStatus + */ + getStatus: { + methodKind: "unary"; + input: typeof GetStatusRequestSchema; + output: typeof CredentialStatusSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.Revoke + */ + revoke: { + methodKind: "unary"; + input: typeof RevokeRequestSchema; + output: typeof CredentialStatusSchema; + }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.Lease + */ + lease: { + methodKind: "unary"; + input: typeof LeaseRequestSchema; + output: typeof LeaseResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_engram_mcp_v1_control, 0); + diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index c273c3c73..b6c5709d8 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -102,7 +102,7 @@ tokio-tungstenite = { version = "0.30", default-features = false, features = ["c tokio-util = { version = "0.7", features = ["codec", "io", "time"] } toml_datetime = { version = "1", features = ["serde"] } toml_parser = { version = "1" } -tonic = { version = "0.14" } +tonic = { version = "0.14", features = ["tls-ring"] } tower = { version = "0.5", default-features = false, features = ["balance", "buffer", "filter", "limit", "load-shed", "log", "retry"] } tracing = { version = "0.1", features = ["log"] } tracing-core = { version = "0.1" } From f1b8485cb8434cb4a00145a98b3c9d76b98b75d8 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 00:03:46 -0500 Subject: [PATCH 012/279] Redact sealed MCP flow state in Debug output --- crates/engram-core/src/types/mcp.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/engram-core/src/types/mcp.rs b/crates/engram-core/src/types/mcp.rs index 4886762cc..fef222648 100644 --- a/crates/engram-core/src/types/mcp.rs +++ b/crates/engram-core/src/types/mcp.rs @@ -1,7 +1,7 @@ //! Coordinator-owned MCP persistence records. No plaintext credential fields. use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct McpSealedState { pub wrapped_dek: Vec, pub nonce: Vec, @@ -9,6 +9,12 @@ pub struct McpSealedState { pub key_id: String, } +impl std::fmt::Debug for McpSealedState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("McpSealedState([redacted])") + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct McpRefreshClaim { pub id: uuid::Uuid, @@ -23,3 +29,20 @@ pub struct McpOAuthAdmission { pub connection_id: String, pub expires_at: chrono::DateTime, } + +#[cfg(test)] +mod tests { + use super::McpSealedState; + + #[test] + fn sealed_state_debug_redacts_all_envelope_fields() { + let state = McpSealedState { + wrapped_dek: vec![11; 32], + nonce: vec![22; 12], + ciphertext: vec![33; 64], + key_id: "test-key-id".into(), + }; + assert_eq!(format!("{state:?}"), "McpSealedState([redacted])"); + assert_eq!(format!("{state:#?}"), "McpSealedState([redacted])"); + } +} From cb287bb43bef1fde52d9e5e2826fd94866893ce0 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 00:31:55 -0500 Subject: [PATCH 013/279] fix(mcp): separate credential delivery and operation deadlines --- crates/engram-coordinator/src/mcp/control.rs | 3 ++ .../engram-coordinator/src/mcp/lifecycle.rs | 4 +-- crates/engram-coordinator/src/mcp/mod.rs | 5 +++- crates/engram-coordinator/src/mcp/model.rs | 3 ++ .../engram-coordinator/tests/mcp_authority.rs | 28 +++++++++++++++++++ .../engram-coordinator/tests/mcp_control.rs | 19 ++++++++++++- crates/engram-coordinator/tests/mcp_oauth.rs | 1 + .../proto/engram/mcp/v1/control.proto | 4 +++ .../src/gen/engram/mcp/v1/control_pb.ts | 16 ++++++++++- web/src/gen/engram/mcp/v1/control_pb.ts | 16 ++++++++++- 10 files changed, 93 insertions(+), 6 deletions(-) diff --git a/crates/engram-coordinator/src/mcp/control.rs b/crates/engram-coordinator/src/mcp/control.rs index 0ce4ef252..857625de7 100644 --- a/crates/engram-coordinator/src/mcp/control.rs +++ b/crates/engram-coordinator/src/mcp/control.rs @@ -94,6 +94,7 @@ impl Control { } fn error(e: McpError) -> Status { match e { + McpError::NotFound => Status::not_found("credential not found"), McpError::Invalid => Status::invalid_argument("invalid credential request"), McpError::Denied => Status::permission_denied("credential request denied"), McpError::Stale => Status::failed_precondition("credential context changed"), @@ -376,6 +377,7 @@ impl CredentialControl for Control { request_digest: v.request_digest, operation: v.operation, expires_at: v.expires_at, + operation_expires_at: v.operation_expires_at, admission: worker::WorkerLease { id: a.id, deployment_id: a.deployment_id, @@ -389,6 +391,7 @@ impl CredentialControl for Control { header: credential.header, value: credential.value, expires_at: lease.expires_at, + operation_expires_at: lease.operation_expires_at, intent_id: lease.intent_id, })) } diff --git a/crates/engram-coordinator/src/mcp/lifecycle.rs b/crates/engram-coordinator/src/mcp/lifecycle.rs index 18c959cbc..5fba7859f 100644 --- a/crates/engram-coordinator/src/mcp/lifecycle.rs +++ b/crates/engram-coordinator/src/mcp/lifecycle.rs @@ -16,7 +16,7 @@ impl McpAuthority { .meta .get_mcp_org_secret_reservation(&reservation.name) .await? - .ok_or(McpError::Unavailable)?; + .ok_or(McpError::NotFound)?; if current.connection_id != connection || current.role != reservation.role { return Err(McpError::Denied); } @@ -47,7 +47,7 @@ impl McpAuthority { provider: registration_id.clone(), }) .await? - .ok_or(McpError::Unavailable)?; + .ok_or(McpError::NotFound)?; let bundle = self.decode_bundle(&row).await?; Ok(CredentialStatus { context: bundle.context, diff --git a/crates/engram-coordinator/src/mcp/mod.rs b/crates/engram-coordinator/src/mcp/mod.rs index 2a0997833..00cf01ae1 100644 --- a/crates/engram-coordinator/src/mcp/mod.rs +++ b/crates/engram-coordinator/src/mcp/mod.rs @@ -6,6 +6,7 @@ pub enum McpError { Invalid, Denied, Unavailable, + NotFound, Stale, Crypto, Transport, @@ -314,7 +315,9 @@ impl McpAuthority { let now = self.manager.clock.now_utc().timestamp_millis(); if lease.expires_at <= now || lease.expires_at > now + 10000 - || lease.admission.expires_at < lease.expires_at + || lease.operation_expires_at < lease.expires_at + || lease.operation_expires_at > now + 300000 + || lease.admission.expires_at < lease.operation_expires_at || lease.admission.expires_at > now + 300000 || lease.admission.deployment_id != self.registry.deployment_id || lease.admission.id.is_empty() diff --git a/crates/engram-coordinator/src/mcp/model.rs b/crates/engram-coordinator/src/mcp/model.rs index ea5c31439..33f5294bb 100644 --- a/crates/engram-coordinator/src/mcp/model.rs +++ b/crates/engram-coordinator/src/mcp/model.rs @@ -54,7 +54,10 @@ pub struct ResourceLease { pub request_digest: String, pub operation: String, pub scope: WorkerScope, + /// Credential delivery deadline, at most ten seconds. pub expires_at: i64, + /// Frozen worker operation deadline; admission must cover the full operation. + pub operation_expires_at: i64, pub admission: WorkerLease, } // Raw values live only in an authenticated request or one operation's memory. diff --git a/crates/engram-coordinator/tests/mcp_authority.rs b/crates/engram-coordinator/tests/mcp_authority.rs index d1ec8dfc1..73ef37f97 100644 --- a/crates/engram-coordinator/tests/mcp_authority.rs +++ b/crates/engram-coordinator/tests/mcp_authority.rs @@ -184,6 +184,21 @@ mod authority { ) } #[tokio::test] + async fn status_missing_authority_is_not_found() { + let (authority, _, _, _) = setup(); + assert!(matches!( + authority + .status( + "connection-a", + &CredentialReference::OAuth { + registration_id: "missing".into() + } + ) + .await, + Err(McpError::NotFound) + )); + } + #[tokio::test] async fn api_rotation_fences_old_lease_even_before_orchestrator_publication() { let (authority, clock, identity, worker) = setup(); let change = ChangeContext { @@ -216,6 +231,7 @@ mod authority { request_id: "request-a".into(), actor_id: "actor-a".into(), }, + operation_expires_at: clock.now_utc().timestamp_millis() + 10000, expires_at: clock.now_utc().timestamp_millis() + 10000, admission: WorkerLease { id: "lease-a".into(), @@ -223,6 +239,18 @@ mod authority { expires_at: clock.now_utc().timestamp_millis() + 10000, }, }; + let mut operation = lease.clone(); + operation.operation_expires_at = clock.now_utc().timestamp_millis() + 60000; + operation.admission.expires_at = operation.operation_expires_at; + assert_eq!( + authority.lease(&operation).await.unwrap().value, + "first-secret" + ); + operation.admission.expires_at -= 1; + assert!(authority.lease(&operation).await.is_err()); + operation.operation_expires_at = clock.now_utc().timestamp_millis() + 300001; + operation.admission.expires_at = operation.operation_expires_at; + assert!(authority.lease(&operation).await.is_err()); assert_eq!(authority.lease(&lease).await.unwrap().value, "first-secret"); let mut next = change; next.previous_authorization_generation = first.context.authorization_generation; diff --git a/crates/engram-coordinator/tests/mcp_control.rs b/crates/engram-coordinator/tests/mcp_control.rs index 3d917bf7d..51d189266 100644 --- a/crates/engram-coordinator/tests/mcp_control.rs +++ b/crates/engram-coordinator/tests/mcp_control.rs @@ -63,6 +63,7 @@ async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() manager, Arc::new(SystemSecurityEntropy), Registry { + enabled: true, deployment_id: "deployment-a".into(), ..Default::default() }, @@ -127,7 +128,23 @@ async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() .connect() .await .unwrap(); - let error = wire::credential_control_client::CredentialControlClient::new(channel) + let mut client = wire::credential_control_client::CredentialControlClient::new(channel); + if pinned { + let missing = client + .get_status(wire::GetStatusRequest { + protocol: 1, + connection_id: "connection-a".into(), + reference: Some(wire::CredentialReference { + kind: wire::CredentialKind::Oauth as i32, + registration_id: "absent".into(), + ..Default::default() + }), + }) + .await + .unwrap_err(); + assert_eq!(missing.code(), tonic::Code::NotFound); + } + let error = client .get_status(wire::GetStatusRequest { protocol: 2, ..Default::default() diff --git a/crates/engram-coordinator/tests/mcp_oauth.rs b/crates/engram-coordinator/tests/mcp_oauth.rs index da25db700..3b6534f38 100644 --- a/crates/engram-coordinator/tests/mcp_oauth.rs +++ b/crates/engram-coordinator/tests/mcp_oauth.rs @@ -204,6 +204,7 @@ fn lease(f: &Fixture, status: &CredentialStatus) -> ResourceLease { request_id: "request-a".into(), actor_id: "actor-a".into(), }, + operation_expires_at: now + 10000, expires_at: now + 10000, admission: WorkerLease { id: "lease-a".into(), diff --git a/crates/engram-protocol/proto/engram/mcp/v1/control.proto b/crates/engram-protocol/proto/engram/mcp/v1/control.proto index c437111b4..5d8149b89 100644 --- a/crates/engram-protocol/proto/engram/mcp/v1/control.proto +++ b/crates/engram-protocol/proto/engram/mcp/v1/control.proto @@ -125,7 +125,10 @@ message LeaseRequest { string intent_id = 5; string request_digest = 6; string operation = 7; + // Short credential delivery deadline, at most ten seconds. int64 expires_at = 8; + // Frozen worker operation deadline, at most five minutes. + int64 operation_expires_at = 12; Admission admission = 9; oneof scope { AdministrationScope administration = 10; @@ -141,4 +144,5 @@ message LeaseResponse { string value = 3; int64 expires_at = 4; string intent_id = 5; + int64 operation_expires_at = 6; } diff --git a/orchestrator/src/gen/engram/mcp/v1/control_pb.ts b/orchestrator/src/gen/engram/mcp/v1/control_pb.ts index 57b3404ce..00333e847 100644 --- a/orchestrator/src/gen/engram/mcp/v1/control_pb.ts +++ b/orchestrator/src/gen/engram/mcp/v1/control_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/control.proto. */ export const file_engram_mcp_v1_control: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJIrIDCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUiZwoNTGVhc2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZoZWFkZXIYAiABKAkSDQoFdmFsdWUYAyABKAkSEgoKZXhwaXJlc19hdBgEIAEoAxIRCglpbnRlbnRfaWQYBSABKAkqjAEKDkNyZWRlbnRpYWxLaW5kEh8KG0NSRURFTlRJQUxfS0lORF9VTlNQRUNJRklFRBAAEhsKF0NSRURFTlRJQUxfS0lORF9BUElfS0VZEAESIQodQ1JFREVOVElBTF9LSU5EX0NMSUVOVF9TRUNSRVQQAhIZChVDUkVERU5USUFMX0tJTkRfT0FVVEgQAzKQBAoRQ3JlZGVudGlhbENvbnRyb2wSTQoJUHV0U2VjcmV0Eh8uZW5ncmFtLm1jcC52MS5QdXRTZWNyZXRSZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEmkKEkJlZ2luQXV0aG9yaXphdGlvbhIoLmVuZ3JhbS5tY3AudjEuQmVnaW5BdXRob3JpemF0aW9uUmVxdWVzdBopLmVuZ3JhbS5tY3AudjEuQmVnaW5BdXRob3JpemF0aW9uUmVzcG9uc2USZQoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEk0KCUdldFN0YXR1cxIfLmVuZ3JhbS5tY3AudjEuR2V0U3RhdHVzUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJHCgZSZXZva2USHC5lbmdyYW0ubWNwLnYxLlJldm9rZVJlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); + fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDKowBCg5DcmVkZW50aWFsS2luZBIfChtDUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIbChdDUkVERU5USUFMX0tJTkRfQVBJX0tFWRABEiEKHUNSRURFTlRJQUxfS0lORF9DTElFTlRfU0VDUkVUEAISGQoVQ1JFREVOVElBTF9LSU5EX09BVVRIEAMykAQKEUNyZWRlbnRpYWxDb250cm9sEk0KCVB1dFNlY3JldBIfLmVuZ3JhbS5tY3AudjEuUHV0U2VjcmV0UmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmUKFUNvbXBsZXRlQXV0aG9yaXphdGlvbhIrLmVuZ3JhbS5tY3AudjEuQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJNCglHZXRTdGF0dXMSHy5lbmdyYW0ubWNwLnYxLkdldFN0YXR1c1JlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSRwoGUmV2b2tlEhwuZW5ncmFtLm1jcC52MS5SZXZva2VSZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEkIKBUxlYXNlEhsuZW5ncmFtLm1jcC52MS5MZWFzZVJlcXVlc3QaHC5lbmdyYW0ubWNwLnYxLkxlYXNlUmVzcG9uc2ViBnByb3RvMw"); /** * @generated from message engram.mcp.v1.Destination @@ -587,10 +587,19 @@ export type LeaseRequest = Message<"engram.mcp.v1.LeaseRequest"> & { operation: string; /** + * Short credential delivery deadline, at most ten seconds. + * * @generated from field: int64 expires_at = 8; */ expiresAt: bigint; + /** + * Frozen worker operation deadline, at most five minutes. + * + * @generated from field: int64 operation_expires_at = 12; + */ + operationExpiresAt: bigint; + /** * @generated from field: engram.mcp.v1.Admission admission = 9; */ @@ -652,6 +661,11 @@ export type LeaseResponse = Message<"engram.mcp.v1.LeaseResponse"> & { * @generated from field: string intent_id = 5; */ intentId: string; + + /** + * @generated from field: int64 operation_expires_at = 6; + */ + operationExpiresAt: bigint; }; /** diff --git a/web/src/gen/engram/mcp/v1/control_pb.ts b/web/src/gen/engram/mcp/v1/control_pb.ts index 57b3404ce..00333e847 100644 --- a/web/src/gen/engram/mcp/v1/control_pb.ts +++ b/web/src/gen/engram/mcp/v1/control_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/control.proto. */ export const file_engram_mcp_v1_control: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJIrIDCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUiZwoNTGVhc2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZoZWFkZXIYAiABKAkSDQoFdmFsdWUYAyABKAkSEgoKZXhwaXJlc19hdBgEIAEoAxIRCglpbnRlbnRfaWQYBSABKAkqjAEKDkNyZWRlbnRpYWxLaW5kEh8KG0NSRURFTlRJQUxfS0lORF9VTlNQRUNJRklFRBAAEhsKF0NSRURFTlRJQUxfS0lORF9BUElfS0VZEAESIQodQ1JFREVOVElBTF9LSU5EX0NMSUVOVF9TRUNSRVQQAhIZChVDUkVERU5USUFMX0tJTkRfT0FVVEgQAzKQBAoRQ3JlZGVudGlhbENvbnRyb2wSTQoJUHV0U2VjcmV0Eh8uZW5ncmFtLm1jcC52MS5QdXRTZWNyZXRSZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEmkKEkJlZ2luQXV0aG9yaXphdGlvbhIoLmVuZ3JhbS5tY3AudjEuQmVnaW5BdXRob3JpemF0aW9uUmVxdWVzdBopLmVuZ3JhbS5tY3AudjEuQmVnaW5BdXRob3JpemF0aW9uUmVzcG9uc2USZQoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEk0KCUdldFN0YXR1cxIfLmVuZ3JhbS5tY3AudjEuR2V0U3RhdHVzUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJHCgZSZXZva2USHC5lbmdyYW0ubWNwLnYxLlJldm9rZVJlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); + fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDKowBCg5DcmVkZW50aWFsS2luZBIfChtDUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIbChdDUkVERU5USUFMX0tJTkRfQVBJX0tFWRABEiEKHUNSRURFTlRJQUxfS0lORF9DTElFTlRfU0VDUkVUEAISGQoVQ1JFREVOVElBTF9LSU5EX09BVVRIEAMykAQKEUNyZWRlbnRpYWxDb250cm9sEk0KCVB1dFNlY3JldBIfLmVuZ3JhbS5tY3AudjEuUHV0U2VjcmV0UmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmUKFUNvbXBsZXRlQXV0aG9yaXphdGlvbhIrLmVuZ3JhbS5tY3AudjEuQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJNCglHZXRTdGF0dXMSHy5lbmdyYW0ubWNwLnYxLkdldFN0YXR1c1JlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSRwoGUmV2b2tlEhwuZW5ncmFtLm1jcC52MS5SZXZva2VSZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEkIKBUxlYXNlEhsuZW5ncmFtLm1jcC52MS5MZWFzZVJlcXVlc3QaHC5lbmdyYW0ubWNwLnYxLkxlYXNlUmVzcG9uc2ViBnByb3RvMw"); /** * @generated from message engram.mcp.v1.Destination @@ -587,10 +587,19 @@ export type LeaseRequest = Message<"engram.mcp.v1.LeaseRequest"> & { operation: string; /** + * Short credential delivery deadline, at most ten seconds. + * * @generated from field: int64 expires_at = 8; */ expiresAt: bigint; + /** + * Frozen worker operation deadline, at most five minutes. + * + * @generated from field: int64 operation_expires_at = 12; + */ + operationExpiresAt: bigint; + /** * @generated from field: engram.mcp.v1.Admission admission = 9; */ @@ -652,6 +661,11 @@ export type LeaseResponse = Message<"engram.mcp.v1.LeaseResponse"> & { * @generated from field: string intent_id = 5; */ intentId: string; + + /** + * @generated from field: int64 operation_expires_at = 6; + */ + operationExpiresAt: bigint; }; /** From d438167ddb9f1b8fa2b64eacd2312a9abd225d51 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 00:32:08 -0500 Subject: [PATCH 014/279] feat(mcp): add fenced credential setup and admitted dispatch adapters --- .../proto/engram/app/v1/integration.proto | 76 +++ docs/adr/0125-isolated-external-mcp.md | 15 + .../drizzle/0104_mcp_credential_metadata.sql | 3 + orchestrator/drizzle/meta/_journal.json | 9 +- .../src/__tests__/mcp-control.test.ts | 105 +++++ .../__tests__/mcp-credential-admin.test.ts | 334 ++++++++++++++ .../__tests__/mcp-credential-broker.test.ts | 281 ++++++++++++ .../__tests__/mcp-credential-oauth.test.ts | 253 ++++++++++ .../__tests__/mcp-credential-store.test.ts | 123 +++++ orchestrator/src/__tests__/mcp-domain.test.ts | 23 + orchestrator/src/__tests__/mcp-rpc.test.ts | 26 +- orchestrator/src/db/schema.ts | 4 + .../src/gen/engram/app/v1/integration_pb.ts | 404 +++++++++++++++- orchestrator/src/mcp/bindings.ts | 4 +- orchestrator/src/mcp/config.ts | 6 +- orchestrator/src/mcp/contracts.ts | 47 +- orchestrator/src/mcp/control-client.ts | 67 +++ orchestrator/src/mcp/credential-admin.ts | 432 ++++++++++++++++++ orchestrator/src/mcp/credential-broker.ts | 138 ++++++ orchestrator/src/mcp/credential-rpc.ts | 178 ++++++++ orchestrator/src/mcp/credential-store.ts | 266 +++++++++++ orchestrator/src/mcp/repository.ts | 3 + orchestrator/src/rpc/integration.ts | 34 +- orchestrator/tsconfig.json | 2 +- ...gration-IntegrationService_connectquery.ts | 20 + web/src/gen/engram/app/v1/integration_pb.ts | 404 +++++++++++++++- 26 files changed, 3209 insertions(+), 48 deletions(-) create mode 100644 orchestrator/drizzle/0104_mcp_credential_metadata.sql create mode 100644 orchestrator/src/__tests__/mcp-control.test.ts create mode 100644 orchestrator/src/__tests__/mcp-credential-admin.test.ts create mode 100644 orchestrator/src/__tests__/mcp-credential-broker.test.ts create mode 100644 orchestrator/src/__tests__/mcp-credential-oauth.test.ts create mode 100644 orchestrator/src/__tests__/mcp-credential-store.test.ts create mode 100644 orchestrator/src/mcp/control-client.ts create mode 100644 orchestrator/src/mcp/credential-admin.ts create mode 100644 orchestrator/src/mcp/credential-broker.ts create mode 100644 orchestrator/src/mcp/credential-rpc.ts create mode 100644 orchestrator/src/mcp/credential-store.ts diff --git a/crates/engram-protocol/proto/engram/app/v1/integration.proto b/crates/engram-protocol/proto/engram/app/v1/integration.proto index ebd22df48..fc687addc 100644 --- a/crates/engram-protocol/proto/engram/app/v1/integration.proto +++ b/crates/engram-protocol/proto/engram/app/v1/integration.proto @@ -54,6 +54,10 @@ service IntegrationService { rpc GetGoogleCloudSetup(GetGoogleCloudSetupRequest) returns (GetGoogleCloudSetupResponse); rpc DiscoverMcp(DiscoverMcpRequest) returns (DiscoverMcpResponse); rpc PublishMcp(PublishMcpRequest) returns (PublishMcpResponse); + rpc PutMcpCredential(PutMcpCredentialRequest) returns (McpCredentialResponse); + rpc GetMcpSetup(GetMcpSetupRequest) returns (GetMcpSetupResponse); + rpc CompleteMcpAuthorization(CompleteMcpAuthorizationRequest) returns (McpCredentialResponse); + rpc RecoverMcpCredential(RecoverMcpCredentialRequest) returns (McpCredentialResponse); rpc AuthorizeMcp(AuthorizeMcpRequest) returns (AuthorizeMcpResponse); rpc DisconnectMcp(DisconnectMcpRequest) returns (DisconnectMcpResponse); rpc ListMcpApprovals(ListMcpApprovalsRequest) returns (ListMcpApprovalsResponse); @@ -112,6 +116,7 @@ message CreateConnectionResponse { } message UpdateConnectionRequest { + uint32 expected_generation = 7; uint32 expected_revision = 6; string id = 1; string alias = 2; @@ -124,6 +129,8 @@ message UpdateConnectionResponse { } message DeleteConnectionRequest { + uint32 expected_revision = 2; + uint32 expected_generation = 3; string id = 1; } message DeleteConnectionResponse { @@ -379,14 +386,20 @@ message PublishMcpResponse { McpCatalog catalog = 1; } message AuthorizeMcpRequest { + uint32 expected_revision = 4; string connection_id = 1; uint32 expected_generation = 2; bool share_with_org_sessions = 3; } message AuthorizeMcpResponse { + uint32 config_revision = 4; + uint32 auth_generation = 5; + string flow_id = 2; + string expires_at = 3; string authorization_url = 1; } message DisconnectMcpRequest { + uint32 expected_revision = 3; string connection_id = 1; uint32 expected_generation = 2; } @@ -437,3 +450,66 @@ message RebindMcpRequest { message RebindMcpResponse { string binding_hash = 1; } + +// Entered values are request-only; responses contain metadata and references. +message PutMcpCredentialRequest { + string connection_id = 1; + uint32 expected_revision = 2; + uint32 expected_generation = 3; + string kind = 4; + string header = 5; + string value = 6; +} +message McpCredentialStatus { + string status = 1; + string reference = 2; + string kind = 3; + string credential_identity = 4; + int64 version = 5; + int64 reservation_generation = 6; +} +message McpCredentialResponse { + IntegrationConnection connection = 1; + McpCredentialStatus credential = 2; +} +message McpApprovedDestination { + string zone_id = 1; + string endpoint = 2; +} +message McpClientRegistration { + string id = 1; + string client_id = 2; + bool confidential = 3; + string redirect_uri = 4; + string issuer = 5; + McpApprovedDestination resource = 6; + repeated string scopes = 7; +} +message GetMcpSetupRequest { + string connection_id = 1; +} +message GetMcpSetupResponse { + bool enabled = 1; + repeated McpClientRegistration registrations = 2; + repeated McpApprovedDestination destinations = 3; + McpCredentialResponse current = 4; + bool transition_pending = 5; + string flow_id = 6; + string flow_expires_at = 7; + bool client_secret_configured = 8; +} +message CompleteMcpAuthorizationRequest { + string connection_id = 1; + uint32 expected_revision = 2; + uint32 expected_generation = 3; + string flow_id = 4; + string state = 5; + string code = 6; + optional string issuer = 7; + string redirect_uri = 8; +} +message RecoverMcpCredentialRequest { + string connection_id = 1; + uint32 expected_revision = 2; + uint32 expected_generation = 3; +} diff --git a/docs/adr/0125-isolated-external-mcp.md b/docs/adr/0125-isolated-external-mcp.md index ba231f55d..d226c4934 100644 --- a/docs/adr/0125-isolated-external-mcp.md +++ b/docs/adr/0125-isolated-external-mcp.md @@ -48,6 +48,13 @@ the frozen operation; a new identity lookup cannot replace a frozen boot identit Worker boot binds an operation, not durable credential authority. After a worker restart, fresh operations can use the current boot with the same sealed authority. +The credential delivery deadline is at most ten seconds. The separately frozen +operation deadline is at most five minutes and remains the worker deadline. +Admission must cover that full operation deadline. Credential control echoes both +deadlines; the orchestrator validates them and dispatches before delivery expires. +Status returns NotFound only for absent authority metadata. Transport and service +failures remain unavailable and cannot justify recovery or replacement. + ### Reviewed catalogs and session binding Reuse named integration connections and profile integration grants. Discovery @@ -128,6 +135,14 @@ transition and revokes the old authority with its exact identity and version. It verifies the durable revocation receipt before acquisition and publication of the replacement. Failed publication cannot restore the old authority. +The existing connection row holds only credential metadata and a durable transition +token. A transition reserves its final config revision before the coordinator seals +a replacement. Publication uses that same revision and compares the transition +token and authorization generation. Recovery changes the token and leaves old +authority revoked. Late control replies and callbacks cannot publish through an +old token. A lost fresh-reference response can leave a sealed orphan; recovery +cannot publish an unknown reference or restore the previous credential. + ### Authorization and irreversible dispatch Before execution, validate the active session/run, immutable binding, principal, diff --git a/orchestrator/drizzle/0104_mcp_credential_metadata.sql b/orchestrator/drizzle/0104_mcp_credential_metadata.sql new file mode 100644 index 000000000..0e00b3cc6 --- /dev/null +++ b/orchestrator/drizzle/0104_mcp_credential_metadata.sql @@ -0,0 +1,3 @@ +-- Coordinator receipts and serialized transition metadata only; no credential values. +ALTER TABLE integration_connection ADD COLUMN mcp_credential jsonb NOT NULL DEFAULT '{}'::jsonb; +ALTER TABLE integration_connection ADD CONSTRAINT integration_connection_mcp_metadata_object CHECK (jsonb_typeof(mcp_credential) = 'object'); diff --git a/orchestrator/drizzle/meta/_journal.json b/orchestrator/drizzle/meta/_journal.json index 6f9af3815..a63cd8f13 100644 --- a/orchestrator/drizzle/meta/_journal.json +++ b/orchestrator/drizzle/meta/_journal.json @@ -729,6 +729,13 @@ "when": 1793585592275, "tag": "0103_mcp_session_retention", "breakpoints": true + }, + { + "idx": 104, + "version": "7", + "when": 1793585592276, + "tag": "0104_mcp_credential_metadata", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/orchestrator/src/__tests__/mcp-control.test.ts b/orchestrator/src/__tests__/mcp-control.test.ts new file mode 100644 index 000000000..62b940e9e --- /dev/null +++ b/orchestrator/src/__tests__/mcp-control.test.ts @@ -0,0 +1,105 @@ +import { expect, test } from "bun:test"; +import { createSecureServer } from "node:http2"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { X509Certificate } from "node:crypto"; +import { connectNodeAdapter } from "@connectrpc/connect-node"; +import { CredentialControl } from "../gen/engram/mcp/v1/control_pb.ts"; +import { makeCredentialControlClient } from "../mcp/control-client.ts"; + +function certificates() { + const dir = mkdtempSync(join(tmpdir(), "mcp-control-test-")); + try { + execFileSync( + "openssl", + [ + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + join(dir, "key"), + "-out", + join(dir, "cert"), + "-days", + "1", + "-subj", + "/CN=localhost", + "-addext", + "subjectAltName=DNS:localhost,IP:127.0.0.1", + "-addext", + "extendedKeyUsage=serverAuth,clientAuth", + ], + { stdio: "ignore" }, + ); + const cert = readFileSync(join(dir, "cert"), "utf8"); + return { + cert, + key: readFileSync(join(dir, "key"), "utf8"), + ca: cert, + serverFingerprint: new X509Certificate(cert).fingerprint256, + }; + } finally { + rmSync(dir, { recursive: true }); + } +} +test("control client requires pinned mTLS and preserves int64 metadata", async () => { + const tls = certificates(); + let calls = 0; + const server = createSecureServer( + { key: tls.key, cert: tls.cert, ca: tls.ca, requestCert: true, rejectUnauthorized: true }, + connectNodeAdapter({ + routes(router) { + router.service(CredentialControl, { + async getStatus(req) { + calls++; + expect(req.protocol).toBe(1); + return { protocol: 1, version: 9007199254740993n, status: "connected" }; + }, + }); + }, + }), + ); + const sessions = new Set(); + server.on("session", (s) => { + sessions.add(s); + s.on("close", () => sessions.delete(s)); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("address"); + const url = `https://127.0.0.1:${address.port}`; + try { + const good = makeCredentialControlClient({ url, ...tls }); + expect((await good.getStatus({ protocol: 1, connectionId: "c" })).version).toBe( + 9007199254740993n, + ); + const bad = makeCredentialControlClient({ + url, + ...tls, + serverFingerprint: "00:".repeat(31) + "00", + }); + await expect(bad.getStatus({ protocol: 1 })).rejects.toThrow("credential control unavailable"); + const other = certificates(); + const untrusted = makeCredentialControlClient({ + url, + ...tls, + key: other.key, + cert: other.cert, + }); + await expect(untrusted.getStatus({ protocol: 1 })).rejects.toThrow( + "credential control unavailable", + ); + expect(calls).toBe(1); + expect(() => + makeCredentialControlClient({ url: url.replace("https:", "http:"), ...tls }), + ).toThrow(); + expect(() => makeCredentialControlClient({ url, ...tls, key: "" })).toThrow(); + } finally { + for (const s of sessions) s.destroy(); + await new Promise((r) => server.close(() => r())); + } +}); diff --git a/orchestrator/src/__tests__/mcp-credential-admin.test.ts b/orchestrator/src/__tests__/mcp-credential-admin.test.ts new file mode 100644 index 000000000..dbc55c6ae --- /dev/null +++ b/orchestrator/src/__tests__/mcp-credential-admin.test.ts @@ -0,0 +1,334 @@ +import { expect, test } from "bun:test"; +import { createClient, createRouterTransport } from "@connectrpc/connect"; +import { create } from "@bufbuild/protobuf"; +import { checkDb } from "../db/client.ts"; +import { makeIntegrationConnectionStore } from "../db/integration-connections.ts"; +import { makeCredentialMetadataStore } from "../mcp/credential-store.ts"; +import { makeCredentialAdmin } from "../mcp/credential-admin.ts"; +import { + CredentialControl, + CredentialKind, + CredentialStatusSchema, + type CredentialStatus, +} from "../gen/engram/mcp/v1/control_pb.ts"; +const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; +const config = { + endpoint: "https://tools.example.test/mcp", + zone: "public", + auth: { kind: "api_key" as const, secretRef: "" }, +}; +async function fixture() { + const connections = makeIntegrationConnectionStore(), + store = makeCredentialMetadataStore(); + const row = await connections.create({ + alias: `mcp-${crypto.randomUUID()}`, + displayName: "Tools", + provider: "mcp", + config, + }); + let current: CredentialStatus | undefined; + let failRevoke = false, + lostRevoke = false; + const calls: string[] = []; + const control = createClient( + CredentialControl, + createRouterTransport((router) => + router.service(CredentialControl, { + async putSecret(req) { + calls.push("put"); + current = create(CredentialStatusSchema, { + protocol: 1, + context: { + connectionId: req.context!.connectionId, + configRevision: req.context!.configRevision, + resource: req.context!.resource, + authorizationGeneration: req.context!.previousAuthorizationGeneration + 1n, + credentialIdentity: `identity-${calls.length}`, + policyDigest: "a".repeat(64), + policyRevision: "v1", + }, + reference: { + kind: req.kind, + name: req.expected?.name || "mcp.fresh", + connectionId: row.id, + generation: (req.expected?.generation ?? 0n) + 1n, + }, + version: (req.expected?.generation ?? 0n) + 1n, + status: "connected", + }); + return current; + }, + async getStatus() { + if (!current) throw new Error("absent"); + return current; + }, + async revoke() { + calls.push("revoke"); + if (failRevoke) throw new Error("refused"); + current = create(CredentialStatusSchema, { + ...current!, + context: { + ...current!.context!, + authorizationGeneration: current!.context!.authorizationGeneration + 1n, + credentialIdentity: "revoked-identity", + }, + reference: { ...current!.reference!, generation: current!.reference!.generation + 1n }, + version: current!.version + 1n, + status: "revoked", + }); + if (lostRevoke) throw new Error("lost reply"); + return current; + }, + }), + ), + ); + const admin = makeCredentialAdmin({ store, control, registrations: [], now: () => new Date() }); + return { + connections, + store, + row, + admin, + calls, + setFailure() { + failRevoke = true; + }, + setLost() { + lostRevoke = true; + }, + async cleanup() { + await connections.delete(row.id); + }, + }; +} +test.skipIf(!reachable)( + "fresh ingestion publishes generated reference at sealed revision and never stores entered value", + async () => { + const f = await fixture(); + try { + await f.admin.putSecret( + f.row.id, + 1, + 1, + "admin", + CredentialKind.API_KEY, + "x-api-key", + "entered-secret", + ); + const row = await f.store.get(f.row.id); + expect(row.config).toEqual({ ...config, auth: { kind: "api_key", secretRef: "mcp.fresh" } }); + expect(row.configRevision).toBe(2); + expect(row.authGeneration).toBe(3); + expect(JSON.stringify(row)).not.toContain("entered-secret"); + await expect( + f.admin.putSecret(f.row.id, 1, 1, "admin", CredentialKind.API_KEY, "x-api-key", "next"), + ).rejects.toThrow("stale"); + expect(f.calls).toEqual(["put"]); + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "failed revocation prevents replacement and leaves old binding disabled", + async () => { + const f = await fixture(); + try { + await f.admin.putSecret( + f.row.id, + 1, + 1, + "admin", + CredentialKind.API_KEY, + "x-api-key", + "first", + ); + f.setFailure(); + await expect( + f.admin.putSecret(f.row.id, 2, 3, "admin", CredentialKind.API_KEY, "x-api-key", "second"), + ).rejects.toThrow(); + expect(f.calls).toEqual(["put", "revoke"]); + const row = await f.store.get(f.row.id); + expect(row.revoked).toBe(true); + expect(row.mcpCredential.transition).toBeDefined(); + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "lost revoke reply is recovered from receipt without replay before rotation", + async () => { + const f = await fixture(); + try { + await f.admin.putSecret( + f.row.id, + 1, + 1, + "admin", + CredentialKind.API_KEY, + "x-api-key", + "first", + ); + f.setLost(); + await f.admin.putSecret( + f.row.id, + 2, + 3, + "admin", + CredentialKind.API_KEY, + "x-api-key", + "second", + ); + expect(f.calls).toEqual(["put", "revoke", "put"]); + const row = await f.store.get(f.row.id); + expect(row.revoked).toBe(false); + expect(row.authGeneration).toBe(5); + } finally { + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "same-reference rotation changes actual identity and preserves published context", + async () => { + const f = await fixture(); + try { + await f.admin.putSecret( + f.row.id, + 1, + 1, + "admin", + CredentialKind.API_KEY, + "x-api-key", + "first", + ); + const first = await f.store.get(f.row.id); + await f.admin.putSecret( + f.row.id, + 2, + 3, + "admin", + CredentialKind.API_KEY, + "x-api-key", + "second", + ); + const next = await f.store.get(f.row.id); + expect(next.config).toEqual(first.config); + expect(next.credentialIdentity).not.toBe(first.credentialIdentity); + expect(next.authGeneration).toBe(5); + expect(next.configRevision).toBe(3); + expect(f.calls).toEqual(["put", "revoke", "put"]); + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "public credential response contains status and references but no entered value", + async () => { + const f = await fixture(); + try { + const { credentialHandlers } = await import("../mcp/credential-rpc.ts"); + const { makeMcpRepository } = await import("../mcp/repository.ts"); + const { makeMcpJournal } = await import("../mcp/journal.ts"); + const { IntegrationService } = await import("../gen/engram/app/v1/integration_pb.ts"); + const client = createClient( + IntegrationService, + createRouterTransport((router) => + router.service( + IntegrationService, + credentialHandlers({ + services: { + enabled: true, + credentials: f.admin, + repository: makeMcpRepository(), + journal: makeMcpJournal(), + }, + connections: f.connections, + getSession: async () => ({ user: { id: "admin", role: "admin" } }), + connectionToProto: (row) => ({ id: row.id }), + }), + ), + ), + ); + const response = await client.putMcpCredential({ + connectionId: f.row.id, + expectedRevision: 1, + expectedGeneration: 1, + kind: "api_key", + header: "x-api-key", + value: "browser-entered-secret", + }); + expect(response.credential!.reference).toBe("mcp.fresh"); + expect(response.credential!.status).toBe("connected"); + expect(response.credential).not.toHaveProperty("value"); + expect(response.credential).not.toHaveProperty("header"); + const setup = await client.getMcpSetup({ connectionId: f.row.id }); + expect(setup.current!.credential!.reference).toBe("mcp.fresh"); + expect(JSON.stringify(await f.store.get(f.row.id))).not.toContain("browser-entered-secret"); + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "named connection CRUD admits an unconfigured draft and refuses browser reference injection", + async () => { + const f = await fixture(); + let createdId: string | undefined; + try { + const { registerIntegration } = await import("../rpc/integration.ts"); + const { makeMcpRepository } = await import("../mcp/repository.ts"); + const { makeMcpJournal } = await import("../mcp/journal.ts"); + const { IntegrationService } = await import("../gen/engram/app/v1/integration_pb.ts"); + const client = createClient( + IntegrationService, + createRouterTransport((router) => + registerIntegration(router, { + getSession: async () => ({ user: { id: "admin", role: "admin" } }), + connections: f.connections, + mcp: { + enabled: true, + credentials: f.admin, + repository: makeMcpRepository(), + journal: makeMcpJournal(), + validateDestination: async () => {}, + }, + }), + ), + ); + const input = { + alias: `mcp-${crypto.randomUUID()}`, + displayName: "Tools", + provider: "mcp", + mcp: { endpoint: config.endpoint, zone: config.zone, authKind: "api_key" }, + }; + await expect( + client.createConnection({ ...input, mcp: { ...input.mcp, apiKeySecretRef: "mcp.stolen" } }), + ).rejects.toThrow("fresh credential"); + const created = await client.createConnection(input); + createdId = created.connection!.id; + expect(created.connection!.enabled).toBe(false); + expect(created.connection!.credentialIdentity).toBe(""); + expect(created.connection!.mcp!.apiKeySecretRef).toBe(""); + await expect( + client.updateConnection({ + id: createdId, + alias: input.alias, + displayName: "Tools", + expectedRevision: 1, + expectedGeneration: 1, + mcp: { ...input.mcp, apiKeySecretRef: "mcp.stolen" }, + }), + ).rejects.toThrow("references cannot"); + expect((await f.store.get(createdId)).configRevision).toBe(1); + await expect( + client.discoverMcp({ connectionId: createdId, expectedRevision: 1 }), + ).rejects.toThrow(); + } finally { + if (createdId) await f.connections.delete(createdId); + await f.cleanup(); + } + }, +); diff --git a/orchestrator/src/__tests__/mcp-credential-broker.test.ts b/orchestrator/src/__tests__/mcp-credential-broker.test.ts new file mode 100644 index 000000000..17f52846b --- /dev/null +++ b/orchestrator/src/__tests__/mcp-credential-broker.test.ts @@ -0,0 +1,281 @@ +import { expect, test } from "bun:test"; +import { createClient, createRouterTransport } from "@connectrpc/connect"; +import { create } from "@bufbuild/protobuf"; +import { + CredentialControl, + CredentialKind, + CredentialStatusSchema, +} from "../gen/engram/mcp/v1/control_pb.ts"; +import { makeCredentialBroker } from "../mcp/credential-broker.ts"; +const now = 100000; +const identity = { + protocol: 1 as const, + bootId: "boot", + zoneId: "public", + policyDigest: "a".repeat(64), + policyRevision: "v1", +}; +const status = create(CredentialStatusSchema, { + protocol: 1, + context: { + connectionId: "connection", + configRevision: 2n, + authorizationGeneration: 3n, + credentialIdentity: "identity", + resource: { zoneId: "public", endpoint: "https://tools.example.test/mcp" }, + policyDigest: identity.policyDigest, + policyRevision: "v1", + }, + reference: { + kind: CredentialKind.API_KEY, + name: "mcp-key", + connectionId: "connection", + generation: 1n, + }, + status: "connected", + version: 1n, +}); +function fixture(expire = false) { + let time = now, + deliveries = 0, + leases = 0; + const seen: unknown[] = []; + const control = createClient( + CredentialControl, + createRouterTransport((router) => + router.service(CredentialControl, { + async lease(req) { + leases++; + seen.push(req); + if (expire) time = now + 10000; + return { + protocol: 1, + header: "x-api-key", + value: "transient-value", + expiresAt: req.expiresAt, + operationExpiresAt: req.operationExpiresAt, + intentId: req.intentId, + }; + }, + }), + ), + ); + const broker = makeCredentialBroker({ + control, + now: () => time, + admission: { + async assertAdmitted() { + return; + }, + }, + worker: { + async identity() { + return identity; + }, + async dispatch(request) { + deliveries++; + return { + ok: true, + identity, + intent: request.intent, + intentId: request.intent.id, + requestDigest: request.intent.requestDigest, + operationDigest: request.intent.operationDigest, + leaseId: request.intent.lease.id, + result: { outcome: "succeeded" }, + }; + }, + }, + }); + const input = { + status, + identity, + intent: { + id: "intent", + bootId: "boot", + zoneId: "public", + policyDigest: identity.policyDigest, + connectionId: "connection", + connectionRevision: 2, + credentialIdentity: "identity", + scope: { + kind: "execution" as const, + sessionId: "s", + runId: "r", + toolCallId: "call", + bindingHash: "b".repeat(64), + }, + operation: "execute" as const, + destination: "https://tools.example.test/mcp", + requestDigest: "b".repeat(64), + expiresAt: now + 60000, + lease: { id: "admission", deploymentId: "deployment", expiresAt: now + 60000 }, + }, + operation: { + kind: "execute" as const, + endpoint: "https://tools.example.test/mcp", + tool: "Read", + arguments: {}, + inputSchema: { type: "object" }, + argumentConstraintDocuments: [], + classification: "read" as const, + }, + }; + return { broker, input, seen, counters: () => ({ deliveries, leases }) }; +} +test("broker preserves 60 second intent while credential delivery expires in 10 seconds", async () => { + const f = fixture(); + await f.broker.dispatch(f.input); + expect(f.counters()).toEqual({ deliveries: 1, leases: 1 }); + expect(f.seen[0]).toMatchObject({ expiresAt: 110000n, operationExpiresAt: 160000n }); +}); +test("expired handoff causes no resource call and no re-lease", async () => { + const f = fixture(true); + await expect(f.broker.dispatch(f.input)).rejects.toThrow("expired"); + expect(f.counters()).toEqual({ deliveries: 0, leases: 1 }); +}); +test("worker policy and boot mismatch prevents secret lease", async () => { + const f = fixture(); + f.input.identity = { ...identity, bootId: "wrong" }; + await expect(f.broker.dispatch(f.input)).rejects.toThrow(); + expect(f.counters()).toEqual({ deliveries: 0, leases: 0 }); +}); +test("admission rejection prevents worker and credential access", async () => { + const f = fixture(); + let touched = false; + const control = createClient( + CredentialControl, + createRouterTransport((router) => + router.service(CredentialControl, { + async lease() { + touched = true; + throw new Error("unexpected"); + }, + }), + ), + ); + const broker = makeCredentialBroker({ + control, + now: () => now, + admission: { + async assertAdmitted() { + throw new Error("admission denied"); + }, + }, + worker: { + async identity() { + touched = true; + return identity; + }, + async dispatch() { + touched = true; + throw new Error("unexpected"); + }, + }, + }); + await expect(broker.dispatch(f.input)).rejects.toThrow("admission denied"); + expect(touched).toBe(false); +}); +test("changed destination, credential identity and insufficient operation admission fail before leasing", async () => { + for (const mutate of [ + (f: ReturnType) => { + f.input.operation.endpoint = "https://other.example.test/"; + }, + (f: ReturnType) => { + f.input.intent.credentialIdentity = "old"; + }, + (f: ReturnType) => { + f.input.intent.lease.expiresAt--; + }, + ]) { + const f = fixture(); + mutate(f); + await expect(f.broker.dispatch(f.input)).rejects.toThrow(); + expect(f.counters()).toEqual({ deliveries: 0, leases: 0 }); + } +}); +test("authority invalidated after lease is checked again before resource dispatch", async () => { + const f = fixture(); + let checks = 0, + sent = false; + const control = createClient( + CredentialControl, + createRouterTransport((router) => + router.service(CredentialControl, { + async lease(req) { + return { + protocol: 1, + header: "x-api-key", + value: "secret", + expiresAt: req.expiresAt, + operationExpiresAt: req.operationExpiresAt, + intentId: req.intentId, + }; + }, + }), + ), + ); + const broker = makeCredentialBroker({ + control, + now: () => now, + admission: { + async assertAdmitted() { + if (++checks === 2) throw new Error("revoked"); + }, + }, + worker: { + async identity() { + return identity; + }, + async dispatch(request) { + sent = true; + return { + ok: true, + identity, + intent: request.intent, + intentId: request.intent.id, + requestDigest: request.intent.requestDigest, + operationDigest: request.intent.operationDigest, + leaseId: request.intent.lease.id, + result: {}, + }; + }, + }, + }); + await expect(broker.dispatch(f.input)).rejects.toThrow("revoked"); + expect(sent).toBe(false); +}); +test("worker transport errors cannot expose the transient credential", async () => { + const f = fixture(); + const control = createClient( + CredentialControl, + createRouterTransport((router) => + router.service(CredentialControl, { + async lease(req) { + return { + protocol: 1, + header: "x-api-key", + value: "secret-value", + expiresAt: req.expiresAt, + operationExpiresAt: req.operationExpiresAt, + intentId: req.intentId, + }; + }, + }), + ), + ); + const broker = makeCredentialBroker({ + control, + now: () => now, + admission: { async assertAdmitted() {} }, + worker: { + async identity() { + return identity; + }, + async dispatch() { + throw new Error("request with secret-value failed"); + }, + }, + }); + await expect(broker.dispatch(f.input)).rejects.toThrow("MCP worker operation unavailable"); +}); diff --git a/orchestrator/src/__tests__/mcp-credential-oauth.test.ts b/orchestrator/src/__tests__/mcp-credential-oauth.test.ts new file mode 100644 index 000000000..eade52e2b --- /dev/null +++ b/orchestrator/src/__tests__/mcp-credential-oauth.test.ts @@ -0,0 +1,253 @@ +import { expect, test } from "bun:test"; +import { Code, ConnectError, createClient, createRouterTransport } from "@connectrpc/connect"; +import { create } from "@bufbuild/protobuf"; +import { checkDb } from "../db/client.ts"; +import { makeIntegrationConnectionStore } from "../db/integration-connections.ts"; +import { makeCredentialMetadataStore } from "../mcp/credential-store.ts"; +import { makeCredentialAdmin } from "../mcp/credential-admin.ts"; +import { + CredentialControl, + CredentialKind, + CredentialStatusSchema, + type AuthorityContext, + type CredentialStatus, +} from "../gen/engram/mcp/v1/control_pb.ts"; +const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; +const config = { + endpoint: "https://tools.example.test/mcp", + zone: "internal", + auth: { + kind: "oauth" as const, + clientRegistrationId: "public-client", + shareWithOrgSessions: true as const, + }, +}; +async function fixture() { + const connections = makeIntegrationConnectionStore(), + store = makeCredentialMetadataStore(); + const row = await connections.create({ + alias: `mcp-${crypto.randomUUID()}`, + displayName: "Tools", + provider: "mcp", + config, + }); + let clock = Date.now(), + flowContext: AuthorityContext | undefined, + current: CredentialStatus | undefined, + lostCallback = false, + outage = false; + const calls: string[] = []; + const control = createClient( + CredentialControl, + createRouterTransport((router) => + router.service(CredentialControl, { + async getStatus() { + if (outage) throw new ConnectError("outage", Code.Unavailable); + if (!current) throw new ConnectError("absent", Code.NotFound); + return current; + }, + async beginAuthorization(req) { + calls.push("begin"); + expect(req.sharingConsent).toBe(true); + expect(req.clientSecret).toBeUndefined(); + expect(req.registrationId).toBe("public-client"); + expect(req.context!.resource!.zoneId).toBe("internal"); + return { + protocol: 1, + flowId: "flow", + context: { + connectionId: row.id, + configRevision: req.context!.configRevision, + authorizationGeneration: req.context!.previousAuthorizationGeneration + 1n, + credentialIdentity: "ceremony", + resource: req.context!.resource, + policyDigest: "a".repeat(64), + policyRevision: "v1", + }, + authorizationUrl: "https://auth.example.test/authorize?state=secret-state", + expiresAt: BigInt(clock + 600000), + }; + }, + async completeAuthorization(req) { + calls.push("complete"); + expect(req.state).toBe("secret-state"); + expect(req.code).toBe("secret-code"); + flowContext = req.context!; + current = create(CredentialStatusSchema, { + protocol: 1, + context: flowContext, + reference: { kind: CredentialKind.OAUTH, registrationId: "public-client" }, + version: (current?.version ?? 0n) + 1n, + status: "connected", + }); + if (lostCallback) throw new ConnectError("lost reply", Code.NotFound); + return current; + }, + async revoke(req) { + calls.push("revoke"); + expect(req.expected!.version).toBe(current!.version); + current = create(CredentialStatusSchema, { + ...current!, + version: current!.version + 1n, + status: "revoked", + }); + return current; + }, + }), + ), + ); + const admin = makeCredentialAdmin({ + store, + control, + now: () => new Date(clock), + registrations: [ + { + id: "public-client", + clientId: "client-id", + confidential: false, + redirectUri: "https://app.example.test/oauth/mcp", + issuer: "https://auth.example.test/", + resource: { zoneId: "internal", endpoint: config.endpoint }, + scopes: ["tools"], + }, + ], + }); + const callback = { + flowId: "flow", + state: "secret-state", + code: "secret-code", + issuer: "https://auth.example.test/", + redirectUri: "https://app.example.test/oauth/mcp", + }; + return { + connections, + store, + row, + admin, + callback, + calls, + lose() { + lostCallback = true; + }, + advance() { + clock += 35000; + }, + outage(value: boolean) { + outage = value; + }, + current: () => current, + async cleanup() { + await connections.delete(row.id); + }, + }; +} +test.skipIf(!reachable)( + "OAuth admin consent, exact callback actor and redirect, one-use publication and metadata-only persistence", + async () => { + const f = await fixture(); + try { + await expect(f.admin.begin(f.row.id, 1, 1, "admin", false)).rejects.toThrow("consent"); + expect(f.calls).toEqual([]); + await f.admin.begin(f.row.id, 1, 1, "admin", true); + let row = await f.store.get(f.row.id); + expect(JSON.stringify(row)).not.toContain("secret-state"); + await expect(f.admin.complete(row.id, 2, 2, "other", f.callback)).rejects.toThrow("stale"); + await expect( + f.admin.complete(row.id, 2, 2, "admin", { + ...f.callback, + redirectUri: "https://evil.example/", + }), + ).rejects.toThrow("callback"); + await f.admin.complete(row.id, 2, 2, "admin", f.callback); + row = await f.store.get(row.id); + expect(row.revoked).toBe(false); + expect(row.authGeneration).toBe(3); + expect(row.configRevision).toBe(2); + expect(JSON.stringify(row)).not.toContain("secret-code"); + expect(JSON.stringify(row)).not.toContain("secret-state"); + await expect(f.admin.complete(row.id, 2, 3, "admin", f.callback)).rejects.toThrow("stale"); + expect(f.calls).toEqual(["begin", "complete"]); + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "switch during pending OAuth blocks the old callback before exchange", + async () => { + const f = await fixture(); + try { + await f.admin.begin(f.row.id, 1, 1, "admin", true); + await f.admin.configure(f.row.id, 2, 2, "admin", { + endpoint: config.endpoint, + zone: "internal", + auth: { kind: "api_key", secretRef: "" }, + }); + await expect(f.admin.complete(f.row.id, 2, 2, "admin", f.callback)).rejects.toThrow(); + expect(f.calls).toEqual(["begin"]); + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "lost callback requires recovery that revokes the published coordinator authority", + async () => { + const f = await fixture(); + try { + await f.admin.begin(f.row.id, 1, 1, "admin", true); + f.lose(); + await expect(f.admin.complete(f.row.id, 2, 2, "admin", f.callback)).rejects.toThrow(); + expect(f.current()!.status).toBe("connected"); + f.advance(); + await f.admin.recover(f.row.id, 2, 2, "admin"); + expect(f.current()!.status).toBe("revoked"); + expect((await f.store.get(f.row.id)).revoked).toBe(true); + expect(f.calls).toEqual(["begin", "complete", "revoke"]); + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "recovery fences a lost reconnect callback over a previously revoked receipt", + async () => { + const f = await fixture(); + try { + await f.admin.begin(f.row.id, 1, 1, "admin", true); + await f.admin.complete(f.row.id, 2, 2, "admin", f.callback); + await f.admin.begin(f.row.id, 2, 3, "admin", true); + f.lose(); + await expect(f.admin.complete(f.row.id, 3, 4, "admin", f.callback)).rejects.toThrow(); + f.advance(); + await f.admin.recover(f.row.id, 3, 4, "admin"); + expect(f.current()!.status).toBe("revoked"); + } finally { + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "recovery keeps its durable fence on outage and clears only confirmed absence", + async () => { + const f = await fixture(); + try { + const now = new Date(); + await f.store.claim(f.row.id, 1, 1, "admin", config, now); + f.advance(); + f.advance(); + f.outage(true); + await expect(f.admin.recover(f.row.id, 2, 2, "admin")).rejects.toThrow(); + expect((await f.store.get(f.row.id)).mcpCredential.transition).toBeDefined(); + f.outage(false); + f.advance(); + await f.admin.recover(f.row.id, 2, 2, "admin"); + const row = await f.store.get(f.row.id); + expect(row.revoked).toBe(true); + expect(row.mcpCredential.transition).toBeUndefined(); + } finally { + await f.cleanup(); + } + }, +); diff --git a/orchestrator/src/__tests__/mcp-credential-store.test.ts b/orchestrator/src/__tests__/mcp-credential-store.test.ts new file mode 100644 index 000000000..cdee3d5e1 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-credential-store.test.ts @@ -0,0 +1,123 @@ +import { expect, test } from "bun:test"; +import { makeIntegrationConnectionStore } from "../db/integration-connections.ts"; +import { makeCredentialMetadataStore } from "../mcp/credential-store.ts"; +import { getDb, checkDb } from "../db/client.ts"; +import { makeMcpRepository } from "../mcp/repository.ts"; +const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; +const config = { + endpoint: "https://tools.example.test/mcp", + zone: "public", + auth: { kind: "api_key", secretRef: "" }, +}; +test.skipIf(!reachable)( + "credential transitions serialize across replicas and publish exact target revision", + async () => { + const connections = makeIntegrationConnectionStore(); + const row = await connections.create({ + alias: `mcp-${crypto.randomUUID()}`, + displayName: "Tools", + provider: "mcp", + config, + }); + const a = makeCredentialMetadataStore(), + b = makeCredentialMetadataStore(); + try { + const now = new Date(); + const results = await Promise.allSettled( + [a, b].map((s) => s.claim(row.id, 1, 1, "admin", config, now)), + ); + expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(1); + const won = results.find((r) => r.status === "fulfilled"); + if (!won || won.status !== "fulfilled") throw new Error("claim"); + const claim = won.value; + const stale = (await connections.get(row.id))!; + expect(stale.mcp!.revoked).toBe(true); + await expect( + makeMcpRepository().updateConfig( + row.id, + stale.mcp!.configRevision, + { alias: row.alias, displayName: "Tools", config }, + now, + ), + ).rejects.toThrow("stale"); + const status = { + protocol: 1, + context: { + connectionId: row.id, + configRevision: String(claim.revision), + authorizationGeneration: String(claim.generation + 1), + credentialIdentity: "identity-new", + resource: { zoneId: "public", endpoint: config.endpoint }, + policyDigest: "a".repeat(64), + policyRevision: "v1", + }, + reference: { + kind: "CREDENTIAL_KIND_API_KEY", + name: "mcp.fresh", + connectionId: row.id, + generation: "1", + }, + version: "1", + status: "connected", + }; + await expect( + a.publish( + claim, + { ...config, auth: { kind: "api_key", secretRef: "mcp.fresh" } }, + { ...status, reference: { ...status.reference, kind: "CREDENTIAL_KIND_CLIENT_SECRET" } }, + now, + ), + ).rejects.toThrow("receipt"); + await a.publish( + claim, + { ...config, auth: { kind: "api_key", secretRef: "mcp.fresh" } }, + status, + now, + ); + const live = (await connections.get(row.id))!; + expect(live.mcp!.configRevision).toBe(claim.revision); + expect(live.mcp!.authGeneration).toBe(claim.generation + 1); + expect(live.mcp!.credentialIdentity).toBe("identity-new"); + expect(live.mcp!.revoked).toBe(false); + await expect(b.publish(claim, config, status, now)).rejects.toThrow("stale"); + } finally { + await connections.delete(row.id); + } + }, +); +test.skipIf(!reachable)( + "recovery replaces durable token and blocks late publication from the old owner", + async () => { + const connections = makeIntegrationConnectionStore(), + a = makeCredentialMetadataStore(), + b = makeCredentialMetadataStore(); + const row = await connections.create({ + alias: `mcp-${crypto.randomUUID()}`, + provider: "mcp", + displayName: "Tools", + config, + }); + try { + const now = new Date(); + const old = await a.claim(row.id, 1, 1, "admin", config, now); + await expect( + b.recover(row.id, old.revision, old.generation, "admin", new Date(now.getTime() + 34999)), + ).rejects.toThrow(); + const recovered = await b.recover( + row.id, + old.revision, + old.generation, + "admin", + new Date(now.getTime() + 35000), + ); + await expect(a.publish(old, config, undefined, now)).rejects.toThrow("stale"); + await b.publish(recovered, config, undefined, now); + const next = await a.claim(row.id, old.revision, old.generation, "admin", config, now); + await expect(b.stage(recovered, {}, now)).rejects.toThrow("stale"); + await a.publish(next, config, undefined, now); + expect((await a.get(row.id)).revoked).toBe(true); + } finally { + await connections.delete(row.id); + } + }, +); diff --git a/orchestrator/src/__tests__/mcp-domain.test.ts b/orchestrator/src/__tests__/mcp-domain.test.ts index ff4d676b9..25719b3c6 100644 --- a/orchestrator/src/__tests__/mcp-domain.test.ts +++ b/orchestrator/src/__tests__/mcp-domain.test.ts @@ -250,3 +250,26 @@ test("child intersections preserve independent local references with colliding d maximum: 5, }); }); +test("API-key draft has no reference and cannot produce a ready binding", () => { + const draft = { ...config, auth: { kind: "api_key" as const, secretRef: "" } }; + expect(validateMcpConfig(draft)).toEqual(draft); + expect( + validateMcpConfig({ + ...config, + auth: { kind: "api_key", secretRef: "mcp-coordinator_generated" }, + }).auth.kind, + ).toBe("api_key"); + expect(() => + compileBindings([ + { + id: "c1", + alias: "tools", + enabled: true, + state, + config: draft, + grants: [grant], + tools: reviewCatalog([tool]), + }, + ]), + ).toThrow("unavailable"); +}); diff --git a/orchestrator/src/__tests__/mcp-rpc.test.ts b/orchestrator/src/__tests__/mcp-rpc.test.ts index bf50e12f9..9dd92dec4 100644 --- a/orchestrator/src/__tests__/mcp-rpc.test.ts +++ b/orchestrator/src/__tests__/mcp-rpc.test.ts @@ -81,14 +81,14 @@ function services(): McpServices { }, }; } -async function spawn(service?: McpServices, role = "admin") { +async function spawn(service?: McpServices, role = "admin", email?: string) { const server = buildServer(new Hono(), (router) => router.service( IntegrationService, mcpHandlers({ services: service, connections, - getSession: async () => ({ user: { id: "owner", role } }), + getSession: async () => ({ user: { id: "owner", role, email } }), now: () => new Date(0), connectionToProto: (c) => ({ id: c.id }), }), @@ -125,6 +125,9 @@ describe("MCP native wire contract", () => { s.client.discoverMcp({ connectionId: row.id, expectedRevision: 1 }), Code.FailedPrecondition, ); + await code(s.client.putMcpCredential({}), Code.FailedPrecondition); + await code(s.client.completeMcpAuthorization({}), Code.FailedPrecondition); + await code(s.client.recoverMcpCredential({}), Code.FailedPrecondition); await code(s.client.publishMcp({}), Code.FailedPrecondition); await code(s.client.authorizeMcp({}), Code.FailedPrecondition); await code(s.client.disconnectMcp({}), Code.FailedPrecondition); @@ -211,3 +214,22 @@ describe("MCP native wire contract", () => { } }); }); + +test("credential lifecycle requires a human administrator before service access", async () => { + const member = await spawn(services(), "user"); + const serviceAccount = await spawn(services(), "admin", "apikey+agent@service.local"); + try { + for (const s of [member, serviceAccount]) { + await code(s.client.putMcpCredential({ value: "entered-value" }), Code.PermissionDenied); + await code(s.client.authorizeMcp({ shareWithOrgSessions: true }), Code.PermissionDenied); + await code( + s.client.completeMcpAuthorization({ code: "provider-code", state: "provider-state" }), + Code.PermissionDenied, + ); + await code(s.client.disconnectMcp({}), Code.PermissionDenied); + } + } finally { + await member.close(); + await serviceAccount.close(); + } +}); diff --git a/orchestrator/src/db/schema.ts b/orchestrator/src/db/schema.ts index a6fab8d78..c5bd7c88d 100644 --- a/orchestrator/src/db/schema.ts +++ b/orchestrator/src/db/schema.ts @@ -1745,6 +1745,10 @@ export const integrationConnection = pgTable( { id: text("id").primaryKey(), alias: text("alias").notNull().unique(), + mcpCredential: jsonb("mcp_credential") + .$type() + .notNull() + .default({}), configRevision: integer("config_revision").notNull().default(1), authGeneration: integer("auth_generation").notNull().default(1), credentialIdentity: text("credential_identity").notNull().default(""), diff --git a/orchestrator/src/gen/engram/app/v1/integration_pb.ts b/orchestrator/src/gen/engram/app/v1/integration_pb.ts index acd7513f6..5d5c0784f 100644 --- a/orchestrator/src/gen/engram/app/v1/integration_pb.ts +++ b/orchestrator/src/gen/engram/app/v1/integration_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/integration.proto. */ export const file_engram_app_v1_integration: GenFile = /*@__PURE__*/ - fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i2AEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAYgASgNEgoKAmlkGAEgASgJEg0KBWFsaWFzGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRJACgxnb29nbGVfY2xvdWQYBCABKAsyKi5lbmdyYW0uYXBwLnYxLkdvb2dsZUNsb3VkQ29ubmVjdGlvbkNvbmZpZxIvCgNtY3AYBSABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENvbm5lY3Rpb25Db25maWciVAoYVXBkYXRlQ29ubmVjdGlvblJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIlChdEZWxldGVDb25uZWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCSIrChhEZWxldGVDb25uZWN0aW9uUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCIjChVUZXN0Q29ubmVjdGlvblJlcXVlc3QSCgoCaWQYASABKAkiNQoWVGVzdENvbm5lY3Rpb25SZXNwb25zZRIKCgJvaxgBIAEoCBIPCgdtZXNzYWdlGAIgASgJIlUKG1NldENvbm5lY3Rpb25FbmFibGVkUmVxdWVzdBIKCgJpZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNIlgKHFNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIigKGkdldEdvb2dsZUNsb3VkU2V0dXBSZXF1ZXN0EgoKAmlkGAEgASgJIqIBChtHZXRHb29nbGVDbG91ZFNldHVwUmVzcG9uc2USDgoGaXNzdWVyGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEhkKEXN1YmplY3RfYXR0cmlidXRlGAMgASgJEhwKFGNvbm5lY3Rpb25fYXR0cmlidXRlGAQgASgJEhUKDWdjbG91ZF9zY3JpcHQYBSABKAkSEQoJdGVycmFmb3JtGAYgASgJIuYBCglDb25uZWN0b3ISEAoIcHJvdmlkZXIYASABKAkSEwoLY29uZmlnX2pzb24YAiABKAkSDwoHYnVpbHRpbhgDIAEoCBISCgpjcmVhdGVkX2F0GAQgASgJEhIKCnVwZGF0ZWRfYXQYBSABKAkSDgoGc3RhdHVzGAYgASgJEjgKCHNldHRpbmdzGAcgAygLMiYuZW5ncmFtLmFwcC52MS5Db25uZWN0b3IuU2V0dGluZ3NFbnRyeRovCg1TZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiFwoVTGlzdENvbm5lY3RvcnNSZXF1ZXN0IkYKFkxpc3RDb25uZWN0b3JzUmVzcG9uc2USLAoKY29ubmVjdG9ycxgBIAMoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIi0KFlVwc2VydENvbm5lY3RvclJlcXVlc3QSEwoLY29uZmlnX2pzb24YASABKAkiRgoXVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiKgoWRGVsZXRlQ29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCSIqChdEZWxldGVDb25uZWN0b3JSZXNwb25zZRIPCgdkZWxldGVkGAEgASgIIjoKDUNvbm5lY3Rvckljb24SDAoEbW9ubxgBIAEoCRINCgVjb2xvchgCIAEoCRIMCgRsb2dvGAMgASgJIn8KEENvbm5lY3RvckRpc3BsYXkSDAoEbmFtZRgBIAEoCRIQCghjYXRlZ29yeRgCIAEoCRINCgVibHVyYhgDIAEoCRIqCgRpY29uGAQgASgLMhwuZW5ncmFtLmFwcC52MS5Db25uZWN0b3JJY29uEhAKCGZlYXR1cmVkGAUgASgIInYKEUNhdGFsb2dDYXBhYmlsaXR5Eg4KBmFjdGlvbhgBIAEoCRIOCgZhY2Nlc3MYAiABKAkSDQoFYXNzZXQYAyABKAkSDQoFbGFiZWwYBCABKAkSDAoEaG9zdBgFIAEoCRIVCg1lbmRwb2ludF9ydWxlGAYgASgJIrQCChRQcm92aWRlckNhdGFsb2dFbnRyeRIQCghwcm92aWRlchgBIAEoCRIwCgdkaXNwbGF5GAIgASgLMh8uZW5ncmFtLmFwcC52MS5Db25uZWN0b3JEaXNwbGF5EhkKEWNyZWRlbnRpYWxfc291cmNlGAMgASgJEg0KBWhvc3RzGAQgAygJEjYKDGNhcGFiaWxpdGllcxgFIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ2F0YWxvZ0NhcGFiaWxpdHkSHQoVZGVmYXVsdF9jb25uZWN0aW9uX2lkGAYgASgJEhgKEGNvbm5lY3Rpb25fbW9kZWwYByABKAkSPQoPdXNlcl9jcmVkZW50aWFsGAggASgLMiQuZW5ncmFtLmFwcC52MS5Vc2VyQ3JlZGVudGlhbFN1cHBvcnQiSQoVVXNlckNyZWRlbnRpYWxTdXBwb3J0Eg0KBW9hdXRoGAEgASgIEg0KBXRva2VuGAIgASgIEhIKCnRva2VuX2hpbnQYAyABKAkiHgocR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVxdWVzdCJXCh1HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXNwb25zZRI2Cglwcm92aWRlcnMYASADKAsyIy5lbmdyYW0uYXBwLnYxLlByb3ZpZGVyQ2F0YWxvZ0VudHJ5Iq4BChhTZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkSDAoEa2luZBgCIAEoCRJDCgZ2YWx1ZXMYAyADKAsyMy5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVxdWVzdC5WYWx1ZXNFbnRyeRotCgtWYWx1ZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjEKGVNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USFAoMc2VjcmV0X25hbWVzGAEgAygJIlAKGlVwbG9hZENvbm5lY3RvckxvZ29SZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGRhdGEYAiABKAwSEgoKbWVkaWFfdHlwZRgDIAEoCSIvChtVcGxvYWRDb25uZWN0b3JMb2dvUmVzcG9uc2USEAoIbG9nb191cmwYASABKAkirgIKFFRlc3RDb25uZWN0b3JSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEkoKDGRyYWZ0X3ZhbHVlcxgCIAMoCzI0LmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QuRHJhZnRWYWx1ZXNFbnRyeRJOCg5kcmFmdF9zZXR0aW5ncxgDIAMoCzI2LmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QuRHJhZnRTZXR0aW5nc0VudHJ5GjIKEERyYWZ0VmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ARo0ChJEcmFmdFNldHRpbmdzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI0ChVUZXN0Q29ubmVjdG9yUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSKsAQobU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEkoKCHNldHRpbmdzGAIgAygLMjguZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QuU2V0dGluZ3NFbnRyeRovCg1TZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiSwocU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXNwb25zZRIrCgljb25uZWN0b3IYASABKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciKrAQoTTWNwQ29ubmVjdGlvbkNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIMCgR6b25lGAIgASgJEhEKCWF1dGhfa2luZBgDIAEoCRIaChJhcGlfa2V5X3NlY3JldF9yZWYYBCABKAkSJAocb2F1dGhfY2xpZW50X3JlZ2lzdHJhdGlvbl9pZBgFIAEoCRIfChdzaGFyZV93aXRoX29yZ19zZXNzaW9ucxgGIAEoCCKqAQoHTWNwVG9vbBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhkKEWlucHV0X3NjaGVtYV9qc29uGAMgASgJEhoKEm91dHB1dF9zY2hlbWFfanNvbhgEIAEoCRIWCg5jbGFzc2lmaWNhdGlvbhgFIAEoCRIPCgdlbmFibGVkGAYgASgIEhMKC2ZpbmdlcnByaW50GAcgASgJIr0BCgpNY3BDYXRhbG9nEgoKAmlkGAEgASgJEhUKDWNvbm5lY3Rpb25faWQYAiABKAkSDAoEa2luZBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoDRIXCg9jb25maWdfcmV2aXNpb24YBSABKA0SFwoPYXV0aF9nZW5lcmF0aW9uGAYgASgNEiUKBXRvb2xzGAcgAygLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEhMKC2ZpbmdlcnByaW50GAggASgJIkYKEkRpc2NvdmVyTWNwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNIkEKE0Rpc2NvdmVyTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyJ+ChFQdWJsaXNoTWNwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhAKCGRyYWZ0X2lkGAIgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEiUKBXRvb2xzGAQgAygLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sIkAKElB1Ymxpc2hNY3BSZXNwb25zZRIqCgdjYXRhbG9nGAEgASgLMhkuZW5ncmFtLmFwcC52MS5NY3BDYXRhbG9nImoKE0F1dGhvcml6ZU1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAIgASgNEh8KF3NoYXJlX3dpdGhfb3JnX3Nlc3Npb25zGAMgASgIIjEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJIkoKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIs4BCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkiLQoXTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJJChhMaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USLQoJYXBwcm92YWxzGAEgAygLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbCJBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgiKgoZRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRINCgVzdGF0ZRgBIAEoCSIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAky3BIKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USVwoMQXV0aG9yaXplTWNwEiIuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXNwb25zZRJaCg1EaXNjb25uZWN0TWNwEiMuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGlzY29ubmVjdE1jcFJlc3BvbnNlEmMKEExpc3RNY3BBcHByb3ZhbHMSJi5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5MaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USZgoRRGVjaWRlTWNwQXBwcm92YWwSJy5lbmdyYW0uYXBwLnYxLkRlY2lkZU1jcEFwcHJvdmFsUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRJaCg1DYW5jZWxNY3BDYWxsEiMuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEmYKEVJlc29sdmVNY3BPdXRjb21lEicuZW5ncmFtLmFwcC52MS5SZXNvbHZlTWNwT3V0Y29tZVJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVzcG9uc2USTgoJUmViaW5kTWNwEh8uZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXNwb25zZWIGcHJvdG8z"); + fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIs4BCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkiLQoXTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJJChhMaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USLQoJYXBwcm92YWxzGAEgAygLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbCJBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgiKgoZRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRINCgVzdGF0ZRgBIAEoCSIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAkilQEKF1B1dE1jcENyZWRlbnRpYWxSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAiABKA0SGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgDIAEoDRIMCgRraW5kGAQgASgJEg4KBmhlYWRlchgFIAEoCRINCgV2YWx1ZRgGIAEoCSKUAQoTTWNwQ3JlZGVudGlhbFN0YXR1cxIOCgZzdGF0dXMYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJEgwKBGtpbmQYAyABKAkSGwoTY3JlZGVudGlhbF9pZGVudGl0eRgEIAEoCRIPCgd2ZXJzaW9uGAUgASgDEh4KFnJlc2VydmF0aW9uX2dlbmVyYXRpb24YBiABKAMiiQEKFU1jcENyZWRlbnRpYWxSZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyI7ChZNY3BBcHByb3ZlZERlc3RpbmF0aW9uEg8KB3pvbmVfaWQYASABKAkSEAoIZW5kcG9pbnQYAiABKAkiuwEKFU1jcENsaWVudFJlZ2lzdHJhdGlvbhIKCgJpZBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSFAoMY29uZmlkZW50aWFsGAMgASgIEhQKDHJlZGlyZWN0X3VyaRgEIAEoCRIOCgZpc3N1ZXIYBSABKAkSNwoIcmVzb3VyY2UYBiABKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SDgoGc2NvcGVzGAcgAygJIisKEkdldE1jcFNldHVwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJIr8CChNHZXRNY3BTZXR1cFJlc3BvbnNlEg8KB2VuYWJsZWQYASABKAgSOwoNcmVnaXN0cmF0aW9ucxgCIAMoCzIkLmVuZ3JhbS5hcHAudjEuTWNwQ2xpZW50UmVnaXN0cmF0aW9uEjsKDGRlc3RpbmF0aW9ucxgDIAMoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92ZWREZXN0aW5hdGlvbhI1CgdjdXJyZW50GAQgASgLMiQuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsUmVzcG9uc2USGgoSdHJhbnNpdGlvbl9wZW5kaW5nGAUgASgIEg8KB2Zsb3dfaWQYBiABKAkSFwoPZmxvd19leHBpcmVzX2F0GAcgASgJEiAKGGNsaWVudF9zZWNyZXRfY29uZmlndXJlZBgIIAEoCCLUAQofQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDwoHZmxvd19pZBgEIAEoCRINCgVzdGF0ZRgFIAEoCRIMCgRjb2RlGAYgASgJEhMKBmlzc3VlchgHIAEoCUgAiAEBEhQKDHJlZGlyZWN0X3VyaRgIIAEoCUIJCgdfaXNzdWVyImwKG1JlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0y8BUKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USYAoQUHV0TWNwQ3JlZGVudGlhbBImLmVuZ3JhbS5hcHAudjEuUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxSZXNwb25zZRJUCgtHZXRNY3BTZXR1cBIhLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRNY3BTZXR1cFJlc3BvbnNlEnAKGENvbXBsZXRlTWNwQXV0aG9yaXphdGlvbhIuLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFJlc3BvbnNlEmgKFFJlY292ZXJNY3BDcmVkZW50aWFsEiouZW5ncmFtLmFwcC52MS5SZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxSZXNwb25zZRJXCgxBdXRob3JpemVNY3ASIi5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlc3BvbnNlEloKDURpc2Nvbm5lY3RNY3ASIy5lbmdyYW0uYXBwLnYxLkRpc2Nvbm5lY3RNY3BSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVzcG9uc2USYwoQTGlzdE1jcEFwcHJvdmFscxImLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRJmChFEZWNpZGVNY3BBcHByb3ZhbBInLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEloKDUNhbmNlbE1jcENhbGwSIy5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVzcG9uc2USZgoRUmVzb2x2ZU1jcE91dGNvbWUSJy5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRJOCglSZWJpbmRNY3ASHy5lbmdyYW0uYXBwLnYxLlJlYmluZE1jcFJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLlJlYmluZE1jcFJlc3BvbnNlYgZwcm90bzM"); /** * @generated from message engram.app.v1.GoogleCloudConnectionConfig @@ -233,6 +233,11 @@ export const CreateConnectionResponseSchema: GenMessage & { + /** + * @generated from field: uint32 expected_generation = 7; + */ + expectedGeneration: number; + /** * @generated from field: uint32 expected_revision = 6; */ @@ -292,6 +297,16 @@ export const UpdateConnectionResponseSchema: GenMessage & { + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; + + /** + * @generated from field: uint32 expected_generation = 3; + */ + expectedGeneration: number; + /** * @generated from field: string id = 1; */ @@ -1321,6 +1336,11 @@ export const PublishMcpResponseSchema: GenMessage = /*@__PUR * @generated from message engram.app.v1.AuthorizeMcpRequest */ export type AuthorizeMcpRequest = Message<"engram.app.v1.AuthorizeMcpRequest"> & { + /** + * @generated from field: uint32 expected_revision = 4; + */ + expectedRevision: number; + /** * @generated from field: string connection_id = 1; */ @@ -1348,6 +1368,26 @@ export const AuthorizeMcpRequestSchema: GenMessage = /*@__P * @generated from message engram.app.v1.AuthorizeMcpResponse */ export type AuthorizeMcpResponse = Message<"engram.app.v1.AuthorizeMcpResponse"> & { + /** + * @generated from field: uint32 config_revision = 4; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 5; + */ + authGeneration: number; + + /** + * @generated from field: string flow_id = 2; + */ + flowId: string; + + /** + * @generated from field: string expires_at = 3; + */ + expiresAt: string; + /** * @generated from field: string authorization_url = 1; */ @@ -1365,6 +1405,11 @@ export const AuthorizeMcpResponseSchema: GenMessage = /*@_ * @generated from message engram.app.v1.DisconnectMcpRequest */ export type DisconnectMcpRequest = Message<"engram.app.v1.DisconnectMcpRequest"> & { + /** + * @generated from field: uint32 expected_revision = 3; + */ + expectedRevision: number; + /** * @generated from field: string connection_id = 1; */ @@ -1642,6 +1687,331 @@ export type RebindMcpResponse = Message<"engram.app.v1.RebindMcpResponse"> & { export const RebindMcpResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_integration, 59); +/** + * Entered values are request-only; responses contain metadata and references. + * + * @generated from message engram.app.v1.PutMcpCredentialRequest + */ +export type PutMcpCredentialRequest = Message<"engram.app.v1.PutMcpCredentialRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; + + /** + * @generated from field: uint32 expected_generation = 3; + */ + expectedGeneration: number; + + /** + * @generated from field: string kind = 4; + */ + kind: string; + + /** + * @generated from field: string header = 5; + */ + header: string; + + /** + * @generated from field: string value = 6; + */ + value: string; +}; + +/** + * Describes the message engram.app.v1.PutMcpCredentialRequest. + * Use `create(PutMcpCredentialRequestSchema)` to create a new message. + */ +export const PutMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 60); + +/** + * @generated from message engram.app.v1.McpCredentialStatus + */ +export type McpCredentialStatus = Message<"engram.app.v1.McpCredentialStatus"> & { + /** + * @generated from field: string status = 1; + */ + status: string; + + /** + * @generated from field: string reference = 2; + */ + reference: string; + + /** + * @generated from field: string kind = 3; + */ + kind: string; + + /** + * @generated from field: string credential_identity = 4; + */ + credentialIdentity: string; + + /** + * @generated from field: int64 version = 5; + */ + version: bigint; + + /** + * @generated from field: int64 reservation_generation = 6; + */ + reservationGeneration: bigint; +}; + +/** + * Describes the message engram.app.v1.McpCredentialStatus. + * Use `create(McpCredentialStatusSchema)` to create a new message. + */ +export const McpCredentialStatusSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 61); + +/** + * @generated from message engram.app.v1.McpCredentialResponse + */ +export type McpCredentialResponse = Message<"engram.app.v1.McpCredentialResponse"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; + + /** + * @generated from field: engram.app.v1.McpCredentialStatus credential = 2; + */ + credential?: McpCredentialStatus; +}; + +/** + * Describes the message engram.app.v1.McpCredentialResponse. + * Use `create(McpCredentialResponseSchema)` to create a new message. + */ +export const McpCredentialResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 62); + +/** + * @generated from message engram.app.v1.McpApprovedDestination + */ +export type McpApprovedDestination = Message<"engram.app.v1.McpApprovedDestination"> & { + /** + * @generated from field: string zone_id = 1; + */ + zoneId: string; + + /** + * @generated from field: string endpoint = 2; + */ + endpoint: string; +}; + +/** + * Describes the message engram.app.v1.McpApprovedDestination. + * Use `create(McpApprovedDestinationSchema)` to create a new message. + */ +export const McpApprovedDestinationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 63); + +/** + * @generated from message engram.app.v1.McpClientRegistration + */ +export type McpClientRegistration = Message<"engram.app.v1.McpClientRegistration"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string client_id = 2; + */ + clientId: string; + + /** + * @generated from field: bool confidential = 3; + */ + confidential: boolean; + + /** + * @generated from field: string redirect_uri = 4; + */ + redirectUri: string; + + /** + * @generated from field: string issuer = 5; + */ + issuer: string; + + /** + * @generated from field: engram.app.v1.McpApprovedDestination resource = 6; + */ + resource?: McpApprovedDestination; + + /** + * @generated from field: repeated string scopes = 7; + */ + scopes: string[]; +}; + +/** + * Describes the message engram.app.v1.McpClientRegistration. + * Use `create(McpClientRegistrationSchema)` to create a new message. + */ +export const McpClientRegistrationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 64); + +/** + * @generated from message engram.app.v1.GetMcpSetupRequest + */ +export type GetMcpSetupRequest = Message<"engram.app.v1.GetMcpSetupRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; +}; + +/** + * Describes the message engram.app.v1.GetMcpSetupRequest. + * Use `create(GetMcpSetupRequestSchema)` to create a new message. + */ +export const GetMcpSetupRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 65); + +/** + * @generated from message engram.app.v1.GetMcpSetupResponse + */ +export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & { + /** + * @generated from field: bool enabled = 1; + */ + enabled: boolean; + + /** + * @generated from field: repeated engram.app.v1.McpClientRegistration registrations = 2; + */ + registrations: McpClientRegistration[]; + + /** + * @generated from field: repeated engram.app.v1.McpApprovedDestination destinations = 3; + */ + destinations: McpApprovedDestination[]; + + /** + * @generated from field: engram.app.v1.McpCredentialResponse current = 4; + */ + current?: McpCredentialResponse; + + /** + * @generated from field: bool transition_pending = 5; + */ + transitionPending: boolean; + + /** + * @generated from field: string flow_id = 6; + */ + flowId: string; + + /** + * @generated from field: string flow_expires_at = 7; + */ + flowExpiresAt: string; + + /** + * @generated from field: bool client_secret_configured = 8; + */ + clientSecretConfigured: boolean; +}; + +/** + * Describes the message engram.app.v1.GetMcpSetupResponse. + * Use `create(GetMcpSetupResponseSchema)` to create a new message. + */ +export const GetMcpSetupResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 66); + +/** + * @generated from message engram.app.v1.CompleteMcpAuthorizationRequest + */ +export type CompleteMcpAuthorizationRequest = Message<"engram.app.v1.CompleteMcpAuthorizationRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; + + /** + * @generated from field: uint32 expected_generation = 3; + */ + expectedGeneration: number; + + /** + * @generated from field: string flow_id = 4; + */ + flowId: string; + + /** + * @generated from field: string state = 5; + */ + state: string; + + /** + * @generated from field: string code = 6; + */ + code: string; + + /** + * @generated from field: optional string issuer = 7; + */ + issuer?: string; + + /** + * @generated from field: string redirect_uri = 8; + */ + redirectUri: string; +}; + +/** + * Describes the message engram.app.v1.CompleteMcpAuthorizationRequest. + * Use `create(CompleteMcpAuthorizationRequestSchema)` to create a new message. + */ +export const CompleteMcpAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 67); + +/** + * @generated from message engram.app.v1.RecoverMcpCredentialRequest + */ +export type RecoverMcpCredentialRequest = Message<"engram.app.v1.RecoverMcpCredentialRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; + + /** + * @generated from field: uint32 expected_generation = 3; + */ + expectedGeneration: number; +}; + +/** + * Describes the message engram.app.v1.RecoverMcpCredentialRequest. + * Use `create(RecoverMcpCredentialRequestSchema)` to create a new message. + */ +export const RecoverMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 68); + /** * ADR 0057 C3: orchestrator-native connector catalog CRUD (Plane B). Connectors * live in the orchestrator DB (the `connector` table, C1); the built-ins @@ -1826,6 +2196,38 @@ export const IntegrationService: GenService<{ input: typeof PublishMcpRequestSchema; output: typeof PublishMcpResponseSchema; }, + /** + * @generated from rpc engram.app.v1.IntegrationService.PutMcpCredential + */ + putMcpCredential: { + methodKind: "unary"; + input: typeof PutMcpCredentialRequestSchema; + output: typeof McpCredentialResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.GetMcpSetup + */ + getMcpSetup: { + methodKind: "unary"; + input: typeof GetMcpSetupRequestSchema; + output: typeof GetMcpSetupResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.CompleteMcpAuthorization + */ + completeMcpAuthorization: { + methodKind: "unary"; + input: typeof CompleteMcpAuthorizationRequestSchema; + output: typeof McpCredentialResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.RecoverMcpCredential + */ + recoverMcpCredential: { + methodKind: "unary"; + input: typeof RecoverMcpCredentialRequestSchema; + output: typeof McpCredentialResponseSchema; + }, /** * @generated from rpc engram.app.v1.IntegrationService.AuthorizeMcp */ diff --git a/orchestrator/src/mcp/bindings.ts b/orchestrator/src/mcp/bindings.ts index 55123fd87..bb4a77d00 100644 --- a/orchestrator/src/mcp/bindings.ts +++ b/orchestrator/src/mcp/bindings.ts @@ -27,7 +27,9 @@ export function compileBindings( const bindings = connections.map((connection) => { if (ids.has(connection.id)) throw new Error("duplicate MCP connection"); ids.add(connection.id); + const config = validateMcpConfig(connection.config); if ( + (config.auth.kind === "api_key" && !config.auth.secretRef) || !connection.enabled || connection.state.revoked || !connection.state.credentialIdentity || @@ -52,7 +54,7 @@ export function compileBindings( const binding = { connectionId: connection.id, ...connection.state, - config: validateMcpConfig(connection.config), + config, tools, policyHash: canonicalDigest(connection.grants), }; diff --git a/orchestrator/src/mcp/config.ts b/orchestrator/src/mcp/config.ts index 7b61265a2..e92f8976c 100644 --- a/orchestrator/src/mcp/config.ts +++ b/orchestrator/src/mcp/config.ts @@ -2,13 +2,15 @@ import { isIP } from "node:net"; import { z } from "zod"; // Naming convention only. Coordinator reservation metadata protects the sealed secret. -const reference = z.string().regex(/^mcp\.[a-zA-Z0-9_.-]{1,160}$/); +const reference = z.string().regex(/^mcp[.-][a-zA-Z0-9_.-]{1,160}$/); const configSchema = z .object({ endpoint: z.string().max(2048), zone: z.string().regex(/^[a-z][a-z0-9-]{0,62}$/), auth: z.discriminatedUnion("kind", [ - z.object({ kind: z.literal("api_key"), secretRef: reference }).strict(), + z + .object({ kind: z.literal("api_key"), secretRef: z.union([reference, z.literal("")]) }) + .strict(), z .object({ kind: z.literal("oauth"), diff --git a/orchestrator/src/mcp/contracts.ts b/orchestrator/src/mcp/contracts.ts index 8f4155750..c81d5abdd 100644 --- a/orchestrator/src/mcp/contracts.ts +++ b/orchestrator/src/mcp/contracts.ts @@ -1,3 +1,5 @@ +import { credentialHandlers } from "./credential-rpc.ts"; +import type { CredentialAdmin } from "./credential-admin.ts"; import { Code, ConnectError, type ServiceImpl } from "@connectrpc/connect"; import type { IntegrationService } from "../gen/engram/app/v1/integration_pb.ts"; import type { @@ -20,13 +22,8 @@ export interface McpServices { validateDestination?: (config: McpConfig) => Promise; validateSchemas?: (tools: readonly McpReviewedTool[]) => Promise; discovery?: { discover(connection: IntegrationConnectionRow): Promise }; - authorization?: { - begin( - connection: IntegrationConnectionRow, - actorId: string, - ): Promise<{ authorizationUrl: string }>; - disconnect(connection: IntegrationConnectionRow): Promise; - }; + credentials?: CredentialAdmin; + destinations?: { zoneId: string; endpoint: string }[]; runtime?: { cancel(executionId: string): Promise; /** Must authorize the live owner/admin and acknowledge deny policy before activation. */ @@ -138,6 +135,10 @@ function parseSchema(value: string): Record { } } type Methods = + | "putMcpCredential" + | "getMcpSetup" + | "completeMcpAuthorization" + | "recoverMcpCredential" | "discoverMcp" | "publishMcp" | "authorizeMcp" @@ -162,6 +163,7 @@ export function mcpHandlers(deps: { return row; } return { + ...credentialHandlers(deps), async discoverMcp(req, ctx) { const actor = await requireAdmin(ctx, deps.getSession); const service = services(); @@ -169,6 +171,8 @@ export function mcpHandlers(deps: { return mcpOperation(async () => { const row = await connection(req.connectionId); if (row.mcp!.configRevision !== req.expectedRevision) throw new Error("stale connection"); + if (row.mcp!.revoked || !row.mcp!.credentialIdentity) + throw new Error("credential setup required"); const tools = await discover.discover(row); return { catalog: catalogToProto( @@ -213,35 +217,6 @@ export function mcpHandlers(deps: { }; }); }, - async authorizeMcp(req, ctx) { - const actor = await requireAdmin(ctx, deps.getSession); - const service = services(); - const authorization = requireMcpHook(service.authorization); - return mcpOperation(async () => { - const row = await connection(req.connectionId); - const config = validateMcpConfig(row.config); - if (!req.shareWithOrgSessions || config.auth.kind !== "oauth") - throw new ConnectError( - "explicit consent to organization session sharing is required", - Code.InvalidArgument, - ); - if (row.mcp!.authGeneration !== req.expectedGeneration) - throw new Error("stale authorization"); - return authorization.begin(row, actor.id); - }); - }, - async disconnectMcp(req, ctx) { - await requireAdmin(ctx, deps.getSession); - const service = services(); - const authorization = requireMcpHook(service.authorization); - return mcpOperation(async () => { - const row = await connection(req.connectionId); - // Revoke local authority before external cleanup, which can fail. - await service.repository.disconnect(row.id, req.expectedGeneration, deps.now()); - await authorization.disconnect(row); - return { connection: deps.connectionToProto(await connection(row.id)) }; - }); - }, async listMcpApprovals(req, ctx) { const actor = await requireUser(ctx, deps.getSession); const service = services(); diff --git a/orchestrator/src/mcp/control-client.ts b/orchestrator/src/mcp/control-client.ts new file mode 100644 index 000000000..a63e31e2d --- /dev/null +++ b/orchestrator/src/mcp/control-client.ts @@ -0,0 +1,67 @@ +import { createClient, ConnectError, Code, type Client } from "@connectrpc/connect"; +import { createGrpcTransport } from "@connectrpc/connect-node"; +import { checkServerIdentity } from "node:tls"; +import { CredentialControl } from "../gen/engram/mcp/v1/control_pb.ts"; + +export type CredentialControlClient = Client; +export interface CredentialControlTls { + url: string; + ca: string; + cert: string; + key: string; + serverFingerprint: string; +} +/** Dedicated transport: explicit trust roots, both certificate identities, no retries. */ +export function makeCredentialControlClient(tls: CredentialControlTls): CredentialControlClient { + const url = new URL(tls.url); + if ( + url.protocol !== "https:" || + url.username || + url.password || + url.search || + url.hash || + url.pathname !== "/" || + !tls.ca || + !tls.cert || + !tls.key || + !/^(?:[0-9A-F]{2}:){31}[0-9A-F]{2}$/.test(tls.serverFingerprint) + ) + throw new Error("MCP credential control requires pinned mTLS"); + return createClient( + CredentialControl, + createGrpcTransport({ + baseUrl: tls.url, + readMaxBytes: 32768, + writeMaxBytes: 32768, + defaultTimeoutMs: 30000, + idleConnectionTimeoutMs: 1000, + nodeOptions: { + ca: tls.ca, + cert: tls.cert, + key: tls.key, + minVersion: "TLSv1.3", + rejectUnauthorized: true, + checkServerIdentity(host, cert) { + return ( + checkServerIdentity(host, cert) ?? + (cert.fingerprint256 === tls.serverFingerprint + ? undefined + : new Error("MCP control certificate mismatch")) + ); + }, + }, + interceptors: [ + (next) => async (request) => { + try { + return await next(request); + } catch (error) { + throw new ConnectError( + "MCP credential control unavailable", + error instanceof ConnectError ? error.code : Code.Unavailable, + ); + } + }, + ], + }), + ); +} diff --git a/orchestrator/src/mcp/credential-admin.ts b/orchestrator/src/mcp/credential-admin.ts new file mode 100644 index 000000000..01ddb8bd7 --- /dev/null +++ b/orchestrator/src/mcp/credential-admin.ts @@ -0,0 +1,432 @@ +import { Code, ConnectError } from "@connectrpc/connect"; +import { create, equals, fromJson, toJson, type JsonObject } from "@bufbuild/protobuf"; +import { + AuthorityContextSchema, + CredentialKind, + CredentialStatusSchema, + type CredentialStatus, +} from "../gen/engram/mcp/v1/control_pb.ts"; +import type { CredentialControlClient } from "./control-client.ts"; +import { + type CredentialMetadataStore, + type CredentialClaim, + statusMetadata, +} from "./credential-store.ts"; +import { validateMcpConfig, type McpConfig } from "./config.ts"; + +export interface McpRegistrationMetadata { + id: string; + clientId: string; + confidential: boolean; + redirectUri: string; + issuer: string; + resource: { zoneId: string; endpoint: string }; + scopes: string[]; +} +function decode(value: JsonObject | undefined) { + return value ? fromJson(CredentialStatusSchema, value) : undefined; +} +function sameAuthority(a: CredentialStatus, b: CredentialStatus) { + return ( + a.protocol === 1 && + b.protocol === 1 && + a.context && + b.context && + a.reference && + b.reference && + equals(AuthorityContextSchema, a.context, b.context) && + a.reference.kind === b.reference.kind && + a.reference.connectionId === b.reference.connectionId && + a.reference.name === b.reference.name && + a.reference.registrationId === b.reference.registrationId + ); +} +function revokedReceipt(old: CredentialStatus, receipt: CredentialStatus) { + if ( + !old.context || + !receipt.context || + !old.reference || + !receipt.reference || + receipt.protocol !== 1 || + receipt.status !== "revoked" || + receipt.version !== old.version + 1n || + receipt.reference.kind !== old.reference.kind || + receipt.reference.name !== old.reference.name || + receipt.reference.registrationId !== old.reference.registrationId || + receipt.reference.connectionId !== old.reference.connectionId + ) + throw new Error("stale revocation receipt"); + if (old.reference.kind === CredentialKind.OAUTH) { + if (!equals(AuthorityContextSchema, old.context, receipt.context)) + throw new Error("stale revocation receipt"); + } else if ( + receipt.reference.generation !== old.reference.generation + 1n || + receipt.context.authorizationGeneration !== old.context.authorizationGeneration + 1n || + receipt.context.credentialIdentity === old.context.credentialIdentity || + !equals( + AuthorityContextSchema, + create(AuthorityContextSchema, { + ...receipt.context, + authorizationGeneration: old.context.authorizationGeneration, + credentialIdentity: old.context.credentialIdentity, + }), + old.context, + ) + ) + throw new Error("stale revocation receipt"); +} +export function makeCredentialAdmin(deps: { + store: CredentialMetadataStore; + control: CredentialControlClient; + registrations: readonly McpRegistrationMetadata[]; + now: () => Date; +}) { + const { store, control, now } = deps; + // Copy only operator metadata. No secret-bearing operator object is retained here. + const registrations = deps.registrations.map((r) => ({ + id: r.id, + clientId: r.clientId, + confidential: r.confidential, + redirectUri: r.redirectUri, + issuer: r.issuer, + resource: { ...r.resource }, + scopes: [...r.scopes], + })); + function registration(config: McpConfig) { + if (config.auth.kind !== "oauth") throw new Error("OAuth connection required"); + const registrationId = config.auth.clientRegistrationId; + const r = registrations.find((r) => r.id === registrationId); + if (!r || r.resource.zoneId !== config.zone || r.resource.endpoint !== config.endpoint) + throw new Error("approved registration required"); + return r; + } + function change(claim: CredentialClaim) { + return { + connectionId: claim.id, + configRevision: BigInt(claim.revision), + previousAuthorizationGeneration: BigInt(claim.generation), + resource: { zoneId: claim.config.zone, endpoint: claim.config.endpoint }, + }; + } + async function revoke(claim: CredentialClaim, field: "status" | "clientSecret", actorId: string) { + const saved = decode(claim.metadata[field]); + if (!saved) return; + const current = await control.getStatus({ + protocol: 1, + connectionId: claim.id, + reference: saved.reference, + }); + if (saved.status === "revoked") { + if (!equals(CredentialStatusSchema, current, saved)) + throw new Error("stale revoked authority"); + return current; + } + // A lost reply can be recovered from the durable receipt, never by replay. + if (current.status === "revoked") { + revokedReceipt(saved, current); + await store.stage(claim, { ...claim.metadata, [field]: statusMetadata(current) }, now()); + return current; + } + if (!sameAuthority(saved, current)) throw new Error("stale credential authority"); + // Normal refresh may change version/status while its identity stays fixed. + await store.stage(claim, { ...claim.metadata, [field]: statusMetadata(current) }, now()); + let receipt: CredentialStatus; + try { + receipt = await control.revoke({ protocol: 1, expected: current, actorId }); + } catch { + receipt = await control.getStatus({ + protocol: 1, + connectionId: claim.id, + reference: current.reference, + }); + } + revokedReceipt(current, receipt); + await store.stage(claim, { ...claim.metadata, [field]: statusMetadata(receipt) }, now()); + return receipt; + } + async function revokeTargetOAuth(claim: CredentialClaim, actorId: string, recover = false) { + if (claim.config.auth.kind !== "oauth") return undefined; + const registrationId = claim.config.auth.clientRegistrationId; + const saved = decode(claim.metadata.status); + if ( + !recover && + saved?.reference?.kind === CredentialKind.OAUTH && + saved.reference.registrationId === registrationId + ) + return revoke(claim, "status", actorId); + let current: CredentialStatus; + try { + current = await control.getStatus({ + protocol: 1, + connectionId: claim.id, + reference: { kind: CredentialKind.OAUTH, registrationId }, + }); + } catch (error) { + if ( + error instanceof ConnectError && + error.code === Code.NotFound && + (!saved || + saved.reference?.kind !== CredentialKind.OAUTH || + saved.reference.registrationId !== registrationId) + ) + return undefined; + throw error; + } + if ( + current.context?.connectionId !== claim.id || + current.reference?.kind !== CredentialKind.OAUTH || + current.reference.registrationId !== registrationId + ) + throw new Error("stale OAuth authority"); + // Recovery owns the connection transition. Revoke the exact current snapshot, + // including a late callback result, without granting or restoring its authority. + await store.stage(claim, { ...claim.metadata, status: statusMetadata(current) }, now()); + return revoke(claim, "status", actorId); + } + async function claimCurrent( + id: string, + revision: number, + generation: number, + actor: string, + config?: McpConfig, + ) { + const row = await store.get(id); + return store.claim(id, revision, generation, actor, config ?? row.config, now()); + } + return { + registrations: () => structuredClone(registrations), + async status(id: string) { + const row = await store.get(id), + stored = decode(row.mcpCredential.status); + const status = stored + ? await control.getStatus({ protocol: 1, connectionId: id, reference: stored.reference }) + : undefined; + return { row, status }; + }, + async putSecret( + id: string, + revision: number, + generation: number, + actorId: string, + kind: CredentialKind, + header: string, + value: string, + ) { + if ( + !value || + value.length > 8192 || + /[\r\n]/.test(value) || + !["authorization", "x-api-key", "api-key"].includes(header) + ) + throw new Error("invalid credential input"); + const row = await store.get(id), + config = validateMcpConfig(row.config); + if ( + kind === CredentialKind.API_KEY + ? config.auth.kind !== "api_key" + : kind !== CredentialKind.CLIENT_SECRET || !registration(config).confidential + ) + throw new Error("credential kind does not match connection"); + const claim = await claimCurrent(id, revision, generation, actorId); + const resource = await revoke(claim, "status", actorId); + const old = + kind === CredentialKind.CLIENT_SECRET + ? await revoke(claim, "clientSecret", actorId) + : resource; + const expected = + old?.reference?.kind === kind && + old.context?.authorizationGeneration === BigInt(claim.generation) + ? old.reference + : undefined; + const result = await control.putSecret({ + protocol: 1, + context: change(claim), + kind, + expected, + header, + value, + actorId, + }); + // Retain the returned reference before publication, so a crash can revoke it. + const field = kind === CredentialKind.API_KEY ? "status" : "clientSecret"; + await store.stage(claim, { ...claim.metadata, [field]: statusMetadata(result) }, now()); + if (kind === CredentialKind.API_KEY) { + if (result.reference?.kind !== CredentialKind.API_KEY || !result.reference.name) + throw new Error("invalid credential receipt"); + await store.publish( + claim, + { ...config, auth: { kind: "api_key", secretRef: result.reference.name } }, + statusMetadata(result), + now(), + ); + } else { + if ( + result.reference?.kind !== CredentialKind.CLIENT_SECRET || + result.context?.connectionId !== id || + result.context.configRevision !== BigInt(claim.revision) || + result.context.authorizationGeneration !== BigInt(claim.generation + 1) + ) + throw new Error("invalid client credential receipt"); + await store.publish(claim, config, undefined, now()); + } + }, + async begin( + id: string, + revision: number, + generation: number, + actorId: string, + consent: boolean, + ) { + const row = await store.get(id), + config = validateMcpConfig(row.config), + r = registration(config); + if (!consent || config.auth.kind !== "oauth" || !config.auth.shareWithOrgSessions) + throw new Error("organization sharing consent required"); + const claim = await claimCurrent(id, revision, generation, actorId); + await revoke(claim, "status", actorId); + const old = await revokeTargetOAuth(claim, actorId); + const client = decode(claim.metadata.clientSecret); + if ( + r.confidential && + (!client || + client.status !== "connected" || + client.reference?.kind !== CredentialKind.CLIENT_SECRET) + ) + throw new Error("client credential required"); + const response = await control.beginAuthorization({ + protocol: 1, + context: change(claim), + registrationId: r.id, + clientSecret: r.confidential ? client!.reference : undefined, + expectedVersion: old?.reference?.registrationId === r.id ? old.version : undefined, + actorId, + sharingConsent: consent, + }); + if ( + response.protocol !== 1 || + !response.context || + response.context.connectionId !== id || + response.context.configRevision !== BigInt(claim.revision) || + response.context.authorizationGeneration !== BigInt(claim.generation + 1) || + response.expiresAt <= BigInt(now().getTime()) || + response.expiresAt > BigInt(now().getTime() + 600000) + ) + throw new Error("invalid authorization receipt"); + const url = new URL(response.authorizationUrl); + if (url.protocol !== "https:" || url.username || url.password || url.hash) + throw new Error("invalid authorization URL"); + await store.stage( + claim, + { + ...claim.metadata, + flow: { + id: response.flowId, + actorId, + registrationId: r.id, + context: toJson(AuthorityContextSchema, response.context) as JsonObject, + expiresAt: Number(response.expiresAt), + }, + }, + now(), + ); + await store.publish(claim, config, undefined, now()); + return { + configRevision: claim.revision, + authGeneration: claim.generation, + authorizationUrl: response.authorizationUrl, + flowId: response.flowId, + expiresAt: new Date(Number(response.expiresAt)).toISOString(), + }; + }, + async complete( + id: string, + revision: number, + generation: number, + actorId: string, + input: { flowId: string; state: string; code: string; issuer?: string; redirectUri: string }, + ) { + const row = await store.get(id), + config = validateMcpConfig(row.config), + r = registration(config); + if ( + input.redirectUri !== r.redirectUri || + (input.issuer !== undefined && input.issuer !== r.issuer) + ) + throw new Error("invalid authorization callback"); + const claim = await store.claim( + id, + revision, + generation, + actorId, + config, + now(), + input.flowId, + ); + const flow = claim.metadata.flow!; + if (flow.registrationId !== r.id) throw new Error("stale authorization registration"); + const result = await control.completeAuthorization({ + protocol: 1, + context: fromJson(AuthorityContextSchema, flow.context), + registrationId: r.id, + flowId: flow.id, + state: input.state, + code: input.code, + issuer: input.issuer, + redirectUri: r.redirectUri, + actorId, + }); + await store.stage(claim, { ...claim.metadata, status: statusMetadata(result) }, now()); + await store.publish(claim, config, statusMetadata(result), now()); + }, + async disconnect(id: string, revision: number, generation: number, actorId: string) { + const claim = await claimCurrent(id, revision, generation, actorId); + await revoke(claim, "status", actorId); + await store.publish(claim, claim.config, undefined, now()); + }, + async configure( + id: string, + revision: number, + generation: number, + actorId: string, + config: McpConfig, + names?: { alias: string; displayName: string }, + ) { + if (config.auth.kind === "oauth") registration(config); + const row = await store.get(id), + current = validateMcpConfig(row.config); + if ( + config.auth.kind === "api_key" && + config.auth.secretRef && + (current.auth.kind !== "api_key" || config.auth.secretRef !== current.auth.secretRef) + ) + throw new Error("fresh credential ingestion required"); + const claim = await claimCurrent(id, revision, generation, actorId, config); + await revoke(claim, "status", actorId); + await revoke(claim, "clientSecret", actorId); + await store.publish(claim, config, undefined, now(), names); + }, + async remove(id: string, revision: number, generation: number, actorId: string) { + const claim = await claimCurrent(id, revision, generation, actorId); + await revoke(claim, "status", actorId); + await revoke(claim, "clientSecret", actorId); + return store.remove(claim); + }, + async recover(id: string, revision: number, generation: number, actorId: string) { + const claim = await store.recover(id, revision, generation, actorId, now()); + if ( + claim.config.auth.kind === "oauth" && + decode(claim.metadata.status)?.reference?.kind === CredentialKind.OAUTH + ) + await revokeTargetOAuth(claim, actorId, true); + else { + await revoke(claim, "status", actorId); + await revokeTargetOAuth(claim, actorId, true); + } + // Client-secret writes also need a durable fence after interrupted publication. + await revoke(claim, "clientSecret", actorId); + delete claim.metadata.flow; + await store.publish(claim, claim.config, undefined, now()); + }, + }; +} +export type CredentialAdmin = ReturnType; diff --git a/orchestrator/src/mcp/credential-broker.ts b/orchestrator/src/mcp/credential-broker.ts new file mode 100644 index 000000000..f610cc311 --- /dev/null +++ b/orchestrator/src/mcp/credential-broker.ts @@ -0,0 +1,138 @@ +import { createHash } from "node:crypto"; +import type { + WorkerIdentity, + WorkerIntent, + WorkerOperation, + WorkerRequest, + WorkerResponse, +} from "../../../mcp-worker/src/contracts.ts"; +import { canonical, parseWorkerRequest } from "../../../mcp-worker/src/index.ts"; +import type { CredentialStatus } from "../gen/engram/mcp/v1/control_pb.ts"; +import type { CredentialControlClient } from "./control-client.ts"; + +export interface McpResourceWorker { + identity(): Promise; + dispatch(request: WorkerRequest): Promise; +} +type ResourceOperation = Extract; +export interface AdmittedCredentialOperation { + status: CredentialStatus; + identity: WorkerIdentity; + intent: Omit; + operation: ResourceOperation; +} +/** The runtime must check live authority and its durable global admission here. */ +export interface McpResourceAdmission { + assertAdmitted(operation: Readonly): Promise; +} +export function makeCredentialBroker(deps: { + control: CredentialControlClient; + worker: McpResourceWorker; + admission: McpResourceAdmission; + now: () => number; +}) { + if (!deps.admission?.assertAdmitted) throw new Error("MCP global admission is required"); + return { + async dispatch(input: AdmittedCredentialOperation): Promise { + const frozen = structuredClone(input), + { status, identity, intent, operation } = frozen; + const context = status.context, + reference = status.reference, + now = deps.now(); + if ( + !context || + !reference || + status.protocol !== 1 || + status.status !== "connected" || + context.connectionId !== intent.connectionId || + context.configRevision !== BigInt(intent.connectionRevision) || + context.credentialIdentity !== intent.credentialIdentity || + context.resource?.endpoint !== intent.destination || + context.resource.zoneId !== intent.zoneId || + identity.protocol !== 1 || + identity.bootId !== intent.bootId || + identity.zoneId !== intent.zoneId || + identity.policyDigest !== intent.policyDigest || + context.policyDigest !== identity.policyDigest || + context.policyRevision !== identity.policyRevision || + operation.endpoint !== intent.destination || + operation.kind !== intent.operation || + "credential" in operation || + intent.expiresAt <= now || + intent.expiresAt > now + 300000 || + intent.lease.expiresAt < intent.expiresAt || + intent.lease.expiresAt > now + 300000 + ) + throw new Error("stale credential dispatch context"); + parseWorkerRequest({ + protocol: 1, + intent: { ...intent, operationDigest: "0".repeat(64) }, + operation, + }); + await deps.admission.assertAdmitted(structuredClone(frozen)); + const actual = await deps.worker.identity(); + if (canonical(actual) !== canonical(identity)) throw new Error("stale worker identity"); + const deliveryExpiry = Math.min(deps.now() + 10000, intent.expiresAt); + if (deliveryExpiry <= deps.now()) throw new Error("credential delivery expired"); + const lease = await deps.control.lease({ + protocol: 1, + context, + reference, + worker: identity, + intentId: intent.id, + requestDigest: intent.requestDigest, + operation: operation.kind, + expiresAt: BigInt(deliveryExpiry), + operationExpiresAt: BigInt(intent.expiresAt), + admission: { ...intent.lease, expiresAt: BigInt(intent.lease.expiresAt) }, + scope: + intent.scope.kind === "execution" + ? { case: "execution", value: intent.scope } + : { case: "administration", value: intent.scope }, + }); + if ( + lease.protocol !== 1 || + lease.intentId !== intent.id || + lease.expiresAt !== BigInt(deliveryExpiry) || + lease.operationExpiresAt !== BigInt(intent.expiresAt) || + !["authorization", "x-api-key", "api-key"].includes(lease.header) || + !lease.value || + /[\r\n]/.test(lease.value) + ) + throw new Error("invalid credential delivery"); + if (deps.now() >= deliveryExpiry) throw new Error("credential delivery expired"); + // parseWorkerRequest validates the credential header union at the wire boundary. + const withCredential = { + ...operation, + credential: { header: lease.header, value: lease.value }, + }; + const operationDigest = createHash("sha256").update(canonical(withCredential)).digest("hex"); + const request = parseWorkerRequest({ + protocol: 1, + intent: { ...intent, operationDigest }, + operation: withCredential, + }); + if (deps.now() >= deliveryExpiry) throw new Error("credential delivery expired"); + await deps.admission.assertAdmitted(structuredClone(frozen)); + if (deps.now() >= deliveryExpiry) throw new Error("credential delivery expired"); + let response: WorkerResponse; + try { + response = await deps.worker.dispatch(request); + } catch { + throw new Error("MCP worker operation unavailable"); + } + // No retry on transport loss or mismatch. Task 5 records uncertain writes. + if ( + !response.ok || + canonical(response.identity) !== canonical(identity) || + canonical(response.intent) !== canonical(request.intent) || + response.intentId !== intent.id || + response.requestDigest !== intent.requestDigest || + response.operationDigest !== operationDigest || + response.leaseId !== intent.lease.id + ) + throw new Error("MCP worker response mismatch"); + return response; + }, + }; +} diff --git a/orchestrator/src/mcp/credential-rpc.ts b/orchestrator/src/mcp/credential-rpc.ts new file mode 100644 index 000000000..3eb0579d3 --- /dev/null +++ b/orchestrator/src/mcp/credential-rpc.ts @@ -0,0 +1,178 @@ +import { Code, ConnectError, type HandlerContext, type ServiceImpl } from "@connectrpc/connect"; +import { fromJson } from "@bufbuild/protobuf"; +import type { IntegrationService } from "../gen/engram/app/v1/integration_pb.ts"; +import { + CredentialKind, + CredentialStatusSchema, + type CredentialStatus, +} from "../gen/engram/mcp/v1/control_pb.ts"; +import { requireAdmin, type GetSession } from "../rpc/require.ts"; +import type { + IntegrationConnectionRow, + IntegrationConnectionStore, +} from "../db/integration-connections.ts"; +import { mcpOperation, requireMcp, requireMcpHook, type McpServices } from "./contracts.ts"; + +export async function requireMcpHumanAdmin(ctx: HandlerContext, getSession: GetSession) { + const actor = await requireAdmin(ctx, getSession); + if (actor.serviceAccount) + throw new ConnectError("a human administrator is required", Code.PermissionDenied); + return actor; +} +function statusToProto(status: CredentialStatus | undefined, revoked: boolean) { + return { + status: revoked ? "disconnected" : (status?.status ?? "not_configured"), + reference: status?.reference?.name || status?.reference?.registrationId || "", + kind: + status?.reference?.kind === CredentialKind.API_KEY + ? "api_key" + : status?.reference?.kind === CredentialKind.OAUTH + ? "oauth" + : "", + credentialIdentity: status?.context?.credentialIdentity ?? "", + version: status?.version ?? 0n, + reservationGeneration: status?.reference?.generation ?? 0n, + }; +} +type Methods = + | "putMcpCredential" + | "getMcpSetup" + | "completeMcpAuthorization" + | "recoverMcpCredential" + | "authorizeMcp" + | "disconnectMcp"; +export function credentialHandlers(deps: { + services?: McpServices; + connections: IntegrationConnectionStore; + getSession: GetSession; + connectionToProto: (row: IntegrationConnectionRow) => { id: string }; +}): Pick, Methods> { + function admin() { + return requireMcpHook(requireMcp(deps.services).credentials); + } + async function result(id: string) { + const row = await deps.connections.get(id); + if (!row || row.provider !== "mcp") + throw new ConnectError("MCP connection not found", Code.NotFound); + const { status } = await admin().status(id); + return { + connection: deps.connectionToProto(row), + credential: statusToProto(status, row.mcp?.revoked ?? true), + }; + } + return { + async putMcpCredential(req, ctx) { + const actor = await requireMcpHumanAdmin(ctx, deps.getSession); + const service = admin(); + const kind = + req.kind === "api_key" + ? CredentialKind.API_KEY + : req.kind === "client_secret" + ? CredentialKind.CLIENT_SECRET + : CredentialKind.UNSPECIFIED; + return mcpOperation(async () => { + await service.putSecret( + req.connectionId, + req.expectedRevision, + req.expectedGeneration, + actor.id, + kind, + req.header, + req.value, + ); + return result(req.connectionId); + }); + }, + async authorizeMcp(req, ctx) { + const actor = await requireMcpHumanAdmin(ctx, deps.getSession); + const service = admin(); + return mcpOperation(() => + service.begin( + req.connectionId, + req.expectedRevision, + req.expectedGeneration, + actor.id, + req.shareWithOrgSessions, + ), + ); + }, + async completeMcpAuthorization(req, ctx) { + const actor = await requireMcpHumanAdmin(ctx, deps.getSession); + const service = admin(); + return mcpOperation(async () => { + await service.complete( + req.connectionId, + req.expectedRevision, + req.expectedGeneration, + actor.id, + { + flowId: req.flowId, + state: req.state, + code: req.code, + issuer: req.issuer, + redirectUri: req.redirectUri, + }, + ); + return result(req.connectionId); + }); + }, + async disconnectMcp(req, ctx) { + const actor = await requireMcpHumanAdmin(ctx, deps.getSession); + const service = admin(); + return mcpOperation(async () => { + await service.disconnect( + req.connectionId, + req.expectedRevision, + req.expectedGeneration, + actor.id, + ); + return result(req.connectionId); + }); + }, + async recoverMcpCredential(req, ctx) { + const actor = await requireMcpHumanAdmin(ctx, deps.getSession); + const service = admin(); + return mcpOperation(async () => { + await service.recover( + req.connectionId, + req.expectedRevision, + req.expectedGeneration, + actor.id, + ); + return result(req.connectionId); + }); + }, + async getMcpSetup(req, ctx) { + await requireMcpHumanAdmin(ctx, deps.getSession); + const service = deps.services; + return mcpOperation(async () => { + const base = { + enabled: service?.enabled ?? false, + registrations: service?.credentials?.registrations() ?? [], + destinations: service?.destinations ?? [], + }; + if (!req.connectionId) return base; + const credentials = requireMcpHook(service?.credentials), + { row, status } = await credentials.status(req.connectionId); + const connection = await deps.connections.get(row.id); + if (!connection) throw new Error("connection not found"); + const client = row.mcpCredential.clientSecret + ? fromJson(CredentialStatusSchema, row.mcpCredential.clientSecret) + : undefined; + return { + ...base, + current: { + connection: deps.connectionToProto(connection), + credential: statusToProto(status, row.revoked), + }, + transitionPending: !!row.mcpCredential.transition, + flowId: row.mcpCredential.flow?.id ?? "", + flowExpiresAt: row.mcpCredential.flow + ? new Date(row.mcpCredential.flow.expiresAt).toISOString() + : "", + clientSecretConfigured: client?.status === "connected", + }; + }); + }, + }; +} diff --git a/orchestrator/src/mcp/credential-store.ts b/orchestrator/src/mcp/credential-store.ts new file mode 100644 index 000000000..b86d74f15 --- /dev/null +++ b/orchestrator/src/mcp/credential-store.ts @@ -0,0 +1,266 @@ +import { and, eq, sql } from "drizzle-orm"; +import { create, fromJson, toJson, type JsonObject } from "@bufbuild/protobuf"; +import { getDb } from "../db/client.ts"; +import { integrationConnection as connection } from "../db/schema.ts"; +import { + CredentialKind, + CredentialStatusSchema, + AuthorityContextSchema, +} from "../gen/engram/mcp/v1/control_pb.ts"; +import { validateMcpConfig, type McpConfig } from "./config.ts"; + +/** Only coordinator metadata. Secret values, callback codes and state never enter this type. */ +export interface CredentialMetadata { + status?: JsonObject; + clientSecret?: JsonObject; + flow?: { + id: string; + actorId: string; + registrationId: string; + context: JsonObject; + expiresAt: number; + }; + transition?: { id: string; actorId: string; startedAt: number }; +} +export interface CredentialClaim { + id: string; + token: string; + revision: number; + generation: number; + metadata: CredentialMetadata; + config: McpConfig; +} +export function statusMetadata( + value: ReturnType>, +): JsonObject { + // Protobuf JSON keeps int64 values exact and excludes transport/request fields. + return toJson(CredentialStatusSchema, value) as JsonObject; +} +function checkedMetadata(value: CredentialMetadata): CredentialMetadata { + return { + ...(value.status + ? { status: statusMetadata(fromJson(CredentialStatusSchema, value.status)) } + : {}), + ...(value.clientSecret + ? { clientSecret: statusMetadata(fromJson(CredentialStatusSchema, value.clientSecret)) } + : {}), + ...(value.flow + ? { + flow: { + id: value.flow.id, + actorId: value.flow.actorId, + registrationId: value.flow.registrationId, + expiresAt: value.flow.expiresAt, + context: toJson( + AuthorityContextSchema, + fromJson(AuthorityContextSchema, value.flow.context), + ) as JsonObject, + }, + } + : {}), + ...(value.transition + ? { + transition: { + id: value.transition.id, + actorId: value.transition.actorId, + startedAt: value.transition.startedAt, + }, + } + : {}), + }; +} +export function makeCredentialMetadataStore(db = getDb()) { + async function get(id: string) { + const [row] = await db + .select() + .from(connection) + .where(and(eq(connection.id, id), eq(connection.provider, "mcp"))); + if (!row) throw new Error("MCP connection not found"); + return row; + } + async function claim( + id: string, + revision: number, + generation: number, + actorId: string, + config: unknown, + now: Date, + callbackId?: string, + ) { + const target = validateMcpConfig(config); + return db.transaction(async (tx) => { + const [row] = await tx + .select() + .from(connection) + .where(and(eq(connection.id, id), eq(connection.provider, "mcp"))) + .for("update"); + if ( + !row || + row.configRevision !== revision || + row.authGeneration !== generation || + row.mcpCredential.transition + ) + throw new Error("stale credential transition"); + if ( + callbackId && + (row.mcpCredential.flow?.id !== callbackId || + row.mcpCredential.flow.actorId !== actorId || + row.mcpCredential.flow.expiresAt <= now.getTime()) + ) + throw new Error("stale authorization flow"); + const metadata = checkedMetadata(row.mcpCredential); + metadata.transition = { id: crypto.randomUUID(), actorId, startedAt: now.getTime() }; + if (!callbackId) delete metadata.flow; + const nextRevision = revision + (callbackId ? 0 : 1), + nextGeneration = generation + (callbackId ? 0 : 1); + await tx + .update(connection) + .set({ + mcpCredential: metadata, + configRevision: nextRevision, + authGeneration: nextGeneration, + revoked: true, + enabled: false, + testedAt: null, + updatedAt: now, + }) + .where(eq(connection.id, id)); + return { + id, + token: metadata.transition.id, + revision: nextRevision, + generation: nextGeneration, + metadata, + config: target, + }; + }); + } + async function stage(claim: CredentialClaim, metadata: CredentialMetadata, now: Date) { + const safe = checkedMetadata({ ...metadata, transition: claim.metadata.transition }); + const [row] = await db + .update(connection) + .set({ mcpCredential: safe, updatedAt: now }) + .where( + and( + eq(connection.id, claim.id), + eq(connection.configRevision, claim.revision), + eq(connection.authGeneration, claim.generation), + sql`${connection.mcpCredential}->'transition'->>'id' = ${claim.token}`, + ), + ) + .returning(); + if (!row) throw new Error("stale credential transition"); + claim.metadata = safe; + } + async function publish( + claim: CredentialClaim, + config: unknown, + status: JsonObject | undefined, + now: Date, + names?: { alias: string; displayName: string }, + ) { + const target = validateMcpConfig(config); + const receipt = status ? fromJson(CredentialStatusSchema, status) : undefined; + if ( + receipt && + (receipt.protocol !== 1 || + receipt.status !== "connected" || + !receipt.context || + receipt.context.connectionId !== claim.id || + receipt.context.configRevision !== BigInt(claim.revision) || + receipt.context.authorizationGeneration !== BigInt(claim.generation + 1) || + receipt.context.resource?.endpoint !== target.endpoint || + receipt.context.resource.zoneId !== target.zone || + !receipt.context.credentialIdentity || + !receipt.reference || + (target.auth.kind === "api_key" + ? receipt.reference.kind !== CredentialKind.API_KEY || + receipt.reference.connectionId !== claim.id || + receipt.reference.name !== target.auth.secretRef + : receipt.reference.kind !== CredentialKind.OAUTH || + receipt.reference.registrationId !== target.auth.clientRegistrationId)) + ) + throw new Error("stale authority receipt"); + const metadata = checkedMetadata({ ...claim.metadata, ...(status ? { status } : {}) }); + delete metadata.transition; + if (receipt) delete metadata.flow; + const [row] = await db + .update(connection) + .set({ + ...names, + config: target, + mcpCredential: metadata, + ...(receipt + ? { + authGeneration: claim.generation + 1, + credentialIdentity: receipt.context!.credentialIdentity, + revoked: false, + } + : {}), + enabled: false, + testedAt: null, + updatedAt: now, + }) + .where( + and( + eq(connection.id, claim.id), + eq(connection.configRevision, claim.revision), + eq(connection.authGeneration, claim.generation), + sql`${connection.mcpCredential}->'transition'->>'id' = ${claim.token}`, + ), + ) + .returning(); + if (!row) throw new Error("stale credential transition"); + return row; + } + return { + get, + claim, + stage, + publish, + async remove(claim: CredentialClaim) { + const rows = await db + .delete(connection) + .where( + and( + eq(connection.id, claim.id), + eq(connection.configRevision, claim.revision), + eq(connection.authGeneration, claim.generation), + sql`${connection.mcpCredential}->'transition'->>'id' = ${claim.token}`, + ), + ) + .returning({ id: connection.id }); + if (!rows.length) throw new Error("stale credential transition"); + return true; + }, + async recover(id: string, revision: number, generation: number, actorId: string, now: Date) { + return db.transaction(async (tx) => { + const [row] = await tx.select().from(connection).where(eq(connection.id, id)).for("update"); + if ( + !row || + row.provider !== "mcp" || + row.configRevision !== revision || + row.authGeneration !== generation || + !row.mcpCredential.transition || + row.mcpCredential.transition.startedAt + 35000 > now.getTime() + ) + throw new Error("stale or active credential transition"); + const metadata = checkedMetadata(row.mcpCredential); + metadata.transition = { id: crypto.randomUUID(), actorId, startedAt: now.getTime() }; + await tx + .update(connection) + .set({ mcpCredential: metadata, updatedAt: now }) + .where(eq(connection.id, id)); + return { + id, + token: metadata.transition.id, + revision, + generation, + metadata, + config: validateMcpConfig(row.config), + }; + }); + }, + }; +} +export type CredentialMetadataStore = ReturnType; diff --git a/orchestrator/src/mcp/repository.ts b/orchestrator/src/mcp/repository.ts index 65b15ff80..8b6770d84 100644 --- a/orchestrator/src/mcp/repository.ts +++ b/orchestrator/src/mcp/repository.ts @@ -31,6 +31,7 @@ export function makeMcpRepository(db = getDb()) { and( eq(connection.id, id), eq(connection.provider, "mcp"), + sql`${connection.mcpCredential}->'transition' IS NULL`, eq(connection.configRevision, expectedRevision), ), ) @@ -62,6 +63,7 @@ export function makeMcpRepository(db = getDb()) { and( eq(connection.id, id), eq(connection.provider, "mcp"), + sql`${connection.mcpCredential}->'transition' IS NULL`, eq(connection.authGeneration, expectedGeneration), eq(connection.configRevision, expectedConfigRevision), ), @@ -84,6 +86,7 @@ export function makeMcpRepository(db = getDb()) { and( eq(connection.id, id), eq(connection.provider, "mcp"), + sql`${connection.mcpCredential}->'transition' IS NULL`, eq(connection.authGeneration, expectedGeneration), ), ) diff --git a/orchestrator/src/rpc/integration.ts b/orchestrator/src/rpc/integration.ts index fb5e80322..64e9c8b75 100644 --- a/orchestrator/src/rpc/integration.ts +++ b/orchestrator/src/rpc/integration.ts @@ -10,6 +10,7 @@ * * Injectable deps (getSession, connectors) for tests. */ +import { requireMcpHumanAdmin } from "../mcp/credential-rpc.ts"; import { mcpHandlers, mcpConfigFromProto, mcpConfigToProto, @@ -737,6 +738,12 @@ export function registerIntegration(router: ConnectRouter, deps?: IntegrationDep async createConnection(req, ctx) { await requireAdmin(ctx, getSession); if (req.provider === "mcp") { + await requireMcpHumanAdmin(ctx, getSession); + if (req.mcp?.apiKeySecretRef) + throw new ConnectError( + "use fresh credential setup after creating the connection", + Code.InvalidArgument, + ); const service = requireMcp(deps?.mcp); const validate = requireMcpHook(service.validateDestination); assertConnectionNames(req.alias, req.displayName); @@ -767,14 +774,23 @@ export function registerIntegration(router: ConnectRouter, deps?: IntegrationDep assertConnectionNames(req.alias, req.displayName); const current = await connections.get(req.id); if (current?.provider === "mcp") { + const actor = await requireMcpHumanAdmin(ctx, getSession); const service = requireMcp(deps?.mcp); const validate = requireMcpHook(service.validateDestination); + const credentials = requireMcpHook(service.credentials); + if (req.mcp?.apiKeySecretRef) + throw new ConnectError("credential references cannot be entered", Code.InvalidArgument); const config = mcpConfigFromProto(req.mcp); return mcpOperation(async () => { await validate(config); - await service.repository.updateConfig(req.id, req.expectedRevision, { - alias: req.alias, displayName: req.displayName.trim(), config, - }, now()); + await credentials.configure( + req.id, + req.expectedRevision, + req.expectedGeneration, + actor.id, + config, + { alias: req.alias, displayName: req.displayName.trim() }, + ); return { connection: connectionToProto((await connections.get(req.id))!) }; }); } @@ -804,6 +820,18 @@ export function registerIntegration(router: ConnectRouter, deps?: IntegrationDep if (referenced) { throw new ConnectError("connection is still granted to a profile", Code.FailedPrecondition); } + if (connection?.provider === "mcp") { + const actor = await requireMcpHumanAdmin(ctx, getSession); + const credentials = requireMcpHook(requireMcp(deps?.mcp).credentials); + return mcpOperation(async () => ({ + deleted: await credentials.remove( + req.id, + req.expectedRevision, + req.expectedGeneration, + actor.id, + ), + })); + } return { deleted: await connections.delete(req.id) }; }, diff --git a/orchestrator/tsconfig.json b/orchestrator/tsconfig.json index 2a5deb6f4..f507c03a8 100644 --- a/orchestrator/tsconfig.json +++ b/orchestrator/tsconfig.json @@ -7,7 +7,7 @@ "skipLibCheck": true, "types": ["bun", "node"], "outDir": "dist", - "rootDir": "src", + "rootDir": "..", "esModuleInterop": true, "resolveJsonModule": true, "allowImportingTsExtensions": true, diff --git a/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts b/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts index 602562582..7eb5ab744 100644 --- a/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts +++ b/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts @@ -126,6 +126,26 @@ export const discoverMcp = IntegrationService.method.discoverMcp; */ export const publishMcp = IntegrationService.method.publishMcp; +/** + * @generated from rpc engram.app.v1.IntegrationService.PutMcpCredential + */ +export const putMcpCredential = IntegrationService.method.putMcpCredential; + +/** + * @generated from rpc engram.app.v1.IntegrationService.GetMcpSetup + */ +export const getMcpSetup = IntegrationService.method.getMcpSetup; + +/** + * @generated from rpc engram.app.v1.IntegrationService.CompleteMcpAuthorization + */ +export const completeMcpAuthorization = IntegrationService.method.completeMcpAuthorization; + +/** + * @generated from rpc engram.app.v1.IntegrationService.RecoverMcpCredential + */ +export const recoverMcpCredential = IntegrationService.method.recoverMcpCredential; + /** * @generated from rpc engram.app.v1.IntegrationService.AuthorizeMcp */ diff --git a/web/src/gen/engram/app/v1/integration_pb.ts b/web/src/gen/engram/app/v1/integration_pb.ts index acd7513f6..5d5c0784f 100644 --- a/web/src/gen/engram/app/v1/integration_pb.ts +++ b/web/src/gen/engram/app/v1/integration_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/integration.proto. */ export const file_engram_app_v1_integration: GenFile = /*@__PURE__*/ - fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i2AEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAYgASgNEgoKAmlkGAEgASgJEg0KBWFsaWFzGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCRJACgxnb29nbGVfY2xvdWQYBCABKAsyKi5lbmdyYW0uYXBwLnYxLkdvb2dsZUNsb3VkQ29ubmVjdGlvbkNvbmZpZxIvCgNtY3AYBSABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENvbm5lY3Rpb25Db25maWciVAoYVXBkYXRlQ29ubmVjdGlvblJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIlChdEZWxldGVDb25uZWN0aW9uUmVxdWVzdBIKCgJpZBgBIAEoCSIrChhEZWxldGVDb25uZWN0aW9uUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCIjChVUZXN0Q29ubmVjdGlvblJlcXVlc3QSCgoCaWQYASABKAkiNQoWVGVzdENvbm5lY3Rpb25SZXNwb25zZRIKCgJvaxgBIAEoCBIPCgdtZXNzYWdlGAIgASgJIlUKG1NldENvbm5lY3Rpb25FbmFibGVkUmVxdWVzdBIKCgJpZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNIlgKHFNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIigKGkdldEdvb2dsZUNsb3VkU2V0dXBSZXF1ZXN0EgoKAmlkGAEgASgJIqIBChtHZXRHb29nbGVDbG91ZFNldHVwUmVzcG9uc2USDgoGaXNzdWVyGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEhkKEXN1YmplY3RfYXR0cmlidXRlGAMgASgJEhwKFGNvbm5lY3Rpb25fYXR0cmlidXRlGAQgASgJEhUKDWdjbG91ZF9zY3JpcHQYBSABKAkSEQoJdGVycmFmb3JtGAYgASgJIuYBCglDb25uZWN0b3ISEAoIcHJvdmlkZXIYASABKAkSEwoLY29uZmlnX2pzb24YAiABKAkSDwoHYnVpbHRpbhgDIAEoCBISCgpjcmVhdGVkX2F0GAQgASgJEhIKCnVwZGF0ZWRfYXQYBSABKAkSDgoGc3RhdHVzGAYgASgJEjgKCHNldHRpbmdzGAcgAygLMiYuZW5ncmFtLmFwcC52MS5Db25uZWN0b3IuU2V0dGluZ3NFbnRyeRovCg1TZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiFwoVTGlzdENvbm5lY3RvcnNSZXF1ZXN0IkYKFkxpc3RDb25uZWN0b3JzUmVzcG9uc2USLAoKY29ubmVjdG9ycxgBIAMoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIi0KFlVwc2VydENvbm5lY3RvclJlcXVlc3QSEwoLY29uZmlnX2pzb24YASABKAkiRgoXVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiKgoWRGVsZXRlQ29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCSIqChdEZWxldGVDb25uZWN0b3JSZXNwb25zZRIPCgdkZWxldGVkGAEgASgIIjoKDUNvbm5lY3Rvckljb24SDAoEbW9ubxgBIAEoCRINCgVjb2xvchgCIAEoCRIMCgRsb2dvGAMgASgJIn8KEENvbm5lY3RvckRpc3BsYXkSDAoEbmFtZRgBIAEoCRIQCghjYXRlZ29yeRgCIAEoCRINCgVibHVyYhgDIAEoCRIqCgRpY29uGAQgASgLMhwuZW5ncmFtLmFwcC52MS5Db25uZWN0b3JJY29uEhAKCGZlYXR1cmVkGAUgASgIInYKEUNhdGFsb2dDYXBhYmlsaXR5Eg4KBmFjdGlvbhgBIAEoCRIOCgZhY2Nlc3MYAiABKAkSDQoFYXNzZXQYAyABKAkSDQoFbGFiZWwYBCABKAkSDAoEaG9zdBgFIAEoCRIVCg1lbmRwb2ludF9ydWxlGAYgASgJIrQCChRQcm92aWRlckNhdGFsb2dFbnRyeRIQCghwcm92aWRlchgBIAEoCRIwCgdkaXNwbGF5GAIgASgLMh8uZW5ncmFtLmFwcC52MS5Db25uZWN0b3JEaXNwbGF5EhkKEWNyZWRlbnRpYWxfc291cmNlGAMgASgJEg0KBWhvc3RzGAQgAygJEjYKDGNhcGFiaWxpdGllcxgFIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ2F0YWxvZ0NhcGFiaWxpdHkSHQoVZGVmYXVsdF9jb25uZWN0aW9uX2lkGAYgASgJEhgKEGNvbm5lY3Rpb25fbW9kZWwYByABKAkSPQoPdXNlcl9jcmVkZW50aWFsGAggASgLMiQuZW5ncmFtLmFwcC52MS5Vc2VyQ3JlZGVudGlhbFN1cHBvcnQiSQoVVXNlckNyZWRlbnRpYWxTdXBwb3J0Eg0KBW9hdXRoGAEgASgIEg0KBXRva2VuGAIgASgIEhIKCnRva2VuX2hpbnQYAyABKAkiHgocR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVxdWVzdCJXCh1HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXNwb25zZRI2Cglwcm92aWRlcnMYASADKAsyIy5lbmdyYW0uYXBwLnYxLlByb3ZpZGVyQ2F0YWxvZ0VudHJ5Iq4BChhTZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkSDAoEa2luZBgCIAEoCRJDCgZ2YWx1ZXMYAyADKAsyMy5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVxdWVzdC5WYWx1ZXNFbnRyeRotCgtWYWx1ZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjEKGVNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USFAoMc2VjcmV0X25hbWVzGAEgAygJIlAKGlVwbG9hZENvbm5lY3RvckxvZ29SZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGRhdGEYAiABKAwSEgoKbWVkaWFfdHlwZRgDIAEoCSIvChtVcGxvYWRDb25uZWN0b3JMb2dvUmVzcG9uc2USEAoIbG9nb191cmwYASABKAkirgIKFFRlc3RDb25uZWN0b3JSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEkoKDGRyYWZ0X3ZhbHVlcxgCIAMoCzI0LmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QuRHJhZnRWYWx1ZXNFbnRyeRJOCg5kcmFmdF9zZXR0aW5ncxgDIAMoCzI2LmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QuRHJhZnRTZXR0aW5nc0VudHJ5GjIKEERyYWZ0VmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ARo0ChJEcmFmdFNldHRpbmdzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI0ChVUZXN0Q29ubmVjdG9yUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSKsAQobU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEkoKCHNldHRpbmdzGAIgAygLMjguZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QuU2V0dGluZ3NFbnRyeRovCg1TZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiSwocU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXNwb25zZRIrCgljb25uZWN0b3IYASABKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciKrAQoTTWNwQ29ubmVjdGlvbkNvbmZpZxIQCghlbmRwb2ludBgBIAEoCRIMCgR6b25lGAIgASgJEhEKCWF1dGhfa2luZBgDIAEoCRIaChJhcGlfa2V5X3NlY3JldF9yZWYYBCABKAkSJAocb2F1dGhfY2xpZW50X3JlZ2lzdHJhdGlvbl9pZBgFIAEoCRIfChdzaGFyZV93aXRoX29yZ19zZXNzaW9ucxgGIAEoCCKqAQoHTWNwVG9vbBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEhkKEWlucHV0X3NjaGVtYV9qc29uGAMgASgJEhoKEm91dHB1dF9zY2hlbWFfanNvbhgEIAEoCRIWCg5jbGFzc2lmaWNhdGlvbhgFIAEoCRIPCgdlbmFibGVkGAYgASgIEhMKC2ZpbmdlcnByaW50GAcgASgJIr0BCgpNY3BDYXRhbG9nEgoKAmlkGAEgASgJEhUKDWNvbm5lY3Rpb25faWQYAiABKAkSDAoEa2luZBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoDRIXCg9jb25maWdfcmV2aXNpb24YBSABKA0SFwoPYXV0aF9nZW5lcmF0aW9uGAYgASgNEiUKBXRvb2xzGAcgAygLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEhMKC2ZpbmdlcnByaW50GAggASgJIkYKEkRpc2NvdmVyTWNwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNIkEKE0Rpc2NvdmVyTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyJ+ChFQdWJsaXNoTWNwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhAKCGRyYWZ0X2lkGAIgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEiUKBXRvb2xzGAQgAygLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sIkAKElB1Ymxpc2hNY3BSZXNwb25zZRIqCgdjYXRhbG9nGAEgASgLMhkuZW5ncmFtLmFwcC52MS5NY3BDYXRhbG9nImoKE0F1dGhvcml6ZU1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAIgASgNEh8KF3NoYXJlX3dpdGhfb3JnX3Nlc3Npb25zGAMgASgIIjEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJIkoKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIs4BCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkiLQoXTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJJChhMaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USLQoJYXBwcm92YWxzGAEgAygLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbCJBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgiKgoZRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRINCgVzdGF0ZRgBIAEoCSIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAky3BIKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USVwoMQXV0aG9yaXplTWNwEiIuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXNwb25zZRJaCg1EaXNjb25uZWN0TWNwEiMuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGlzY29ubmVjdE1jcFJlc3BvbnNlEmMKEExpc3RNY3BBcHByb3ZhbHMSJi5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5MaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USZgoRRGVjaWRlTWNwQXBwcm92YWwSJy5lbmdyYW0uYXBwLnYxLkRlY2lkZU1jcEFwcHJvdmFsUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRJaCg1DYW5jZWxNY3BDYWxsEiMuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEmYKEVJlc29sdmVNY3BPdXRjb21lEicuZW5ncmFtLmFwcC52MS5SZXNvbHZlTWNwT3V0Y29tZVJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVzcG9uc2USTgoJUmViaW5kTWNwEh8uZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXNwb25zZWIGcHJvdG8z"); + fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIs4BCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkiLQoXTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJJChhMaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USLQoJYXBwcm92YWxzGAEgAygLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbCJBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgiKgoZRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRINCgVzdGF0ZRgBIAEoCSIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAkilQEKF1B1dE1jcENyZWRlbnRpYWxSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAiABKA0SGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgDIAEoDRIMCgRraW5kGAQgASgJEg4KBmhlYWRlchgFIAEoCRINCgV2YWx1ZRgGIAEoCSKUAQoTTWNwQ3JlZGVudGlhbFN0YXR1cxIOCgZzdGF0dXMYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJEgwKBGtpbmQYAyABKAkSGwoTY3JlZGVudGlhbF9pZGVudGl0eRgEIAEoCRIPCgd2ZXJzaW9uGAUgASgDEh4KFnJlc2VydmF0aW9uX2dlbmVyYXRpb24YBiABKAMiiQEKFU1jcENyZWRlbnRpYWxSZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyI7ChZNY3BBcHByb3ZlZERlc3RpbmF0aW9uEg8KB3pvbmVfaWQYASABKAkSEAoIZW5kcG9pbnQYAiABKAkiuwEKFU1jcENsaWVudFJlZ2lzdHJhdGlvbhIKCgJpZBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSFAoMY29uZmlkZW50aWFsGAMgASgIEhQKDHJlZGlyZWN0X3VyaRgEIAEoCRIOCgZpc3N1ZXIYBSABKAkSNwoIcmVzb3VyY2UYBiABKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SDgoGc2NvcGVzGAcgAygJIisKEkdldE1jcFNldHVwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJIr8CChNHZXRNY3BTZXR1cFJlc3BvbnNlEg8KB2VuYWJsZWQYASABKAgSOwoNcmVnaXN0cmF0aW9ucxgCIAMoCzIkLmVuZ3JhbS5hcHAudjEuTWNwQ2xpZW50UmVnaXN0cmF0aW9uEjsKDGRlc3RpbmF0aW9ucxgDIAMoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92ZWREZXN0aW5hdGlvbhI1CgdjdXJyZW50GAQgASgLMiQuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsUmVzcG9uc2USGgoSdHJhbnNpdGlvbl9wZW5kaW5nGAUgASgIEg8KB2Zsb3dfaWQYBiABKAkSFwoPZmxvd19leHBpcmVzX2F0GAcgASgJEiAKGGNsaWVudF9zZWNyZXRfY29uZmlndXJlZBgIIAEoCCLUAQofQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDwoHZmxvd19pZBgEIAEoCRINCgVzdGF0ZRgFIAEoCRIMCgRjb2RlGAYgASgJEhMKBmlzc3VlchgHIAEoCUgAiAEBEhQKDHJlZGlyZWN0X3VyaRgIIAEoCUIJCgdfaXNzdWVyImwKG1JlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0y8BUKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USYAoQUHV0TWNwQ3JlZGVudGlhbBImLmVuZ3JhbS5hcHAudjEuUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxSZXNwb25zZRJUCgtHZXRNY3BTZXR1cBIhLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRNY3BTZXR1cFJlc3BvbnNlEnAKGENvbXBsZXRlTWNwQXV0aG9yaXphdGlvbhIuLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFJlc3BvbnNlEmgKFFJlY292ZXJNY3BDcmVkZW50aWFsEiouZW5ncmFtLmFwcC52MS5SZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxSZXNwb25zZRJXCgxBdXRob3JpemVNY3ASIi5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlc3BvbnNlEloKDURpc2Nvbm5lY3RNY3ASIy5lbmdyYW0uYXBwLnYxLkRpc2Nvbm5lY3RNY3BSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVzcG9uc2USYwoQTGlzdE1jcEFwcHJvdmFscxImLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRJmChFEZWNpZGVNY3BBcHByb3ZhbBInLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEloKDUNhbmNlbE1jcENhbGwSIy5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVzcG9uc2USZgoRUmVzb2x2ZU1jcE91dGNvbWUSJy5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRJOCglSZWJpbmRNY3ASHy5lbmdyYW0uYXBwLnYxLlJlYmluZE1jcFJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLlJlYmluZE1jcFJlc3BvbnNlYgZwcm90bzM"); /** * @generated from message engram.app.v1.GoogleCloudConnectionConfig @@ -233,6 +233,11 @@ export const CreateConnectionResponseSchema: GenMessage & { + /** + * @generated from field: uint32 expected_generation = 7; + */ + expectedGeneration: number; + /** * @generated from field: uint32 expected_revision = 6; */ @@ -292,6 +297,16 @@ export const UpdateConnectionResponseSchema: GenMessage & { + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; + + /** + * @generated from field: uint32 expected_generation = 3; + */ + expectedGeneration: number; + /** * @generated from field: string id = 1; */ @@ -1321,6 +1336,11 @@ export const PublishMcpResponseSchema: GenMessage = /*@__PUR * @generated from message engram.app.v1.AuthorizeMcpRequest */ export type AuthorizeMcpRequest = Message<"engram.app.v1.AuthorizeMcpRequest"> & { + /** + * @generated from field: uint32 expected_revision = 4; + */ + expectedRevision: number; + /** * @generated from field: string connection_id = 1; */ @@ -1348,6 +1368,26 @@ export const AuthorizeMcpRequestSchema: GenMessage = /*@__P * @generated from message engram.app.v1.AuthorizeMcpResponse */ export type AuthorizeMcpResponse = Message<"engram.app.v1.AuthorizeMcpResponse"> & { + /** + * @generated from field: uint32 config_revision = 4; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 5; + */ + authGeneration: number; + + /** + * @generated from field: string flow_id = 2; + */ + flowId: string; + + /** + * @generated from field: string expires_at = 3; + */ + expiresAt: string; + /** * @generated from field: string authorization_url = 1; */ @@ -1365,6 +1405,11 @@ export const AuthorizeMcpResponseSchema: GenMessage = /*@_ * @generated from message engram.app.v1.DisconnectMcpRequest */ export type DisconnectMcpRequest = Message<"engram.app.v1.DisconnectMcpRequest"> & { + /** + * @generated from field: uint32 expected_revision = 3; + */ + expectedRevision: number; + /** * @generated from field: string connection_id = 1; */ @@ -1642,6 +1687,331 @@ export type RebindMcpResponse = Message<"engram.app.v1.RebindMcpResponse"> & { export const RebindMcpResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_integration, 59); +/** + * Entered values are request-only; responses contain metadata and references. + * + * @generated from message engram.app.v1.PutMcpCredentialRequest + */ +export type PutMcpCredentialRequest = Message<"engram.app.v1.PutMcpCredentialRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; + + /** + * @generated from field: uint32 expected_generation = 3; + */ + expectedGeneration: number; + + /** + * @generated from field: string kind = 4; + */ + kind: string; + + /** + * @generated from field: string header = 5; + */ + header: string; + + /** + * @generated from field: string value = 6; + */ + value: string; +}; + +/** + * Describes the message engram.app.v1.PutMcpCredentialRequest. + * Use `create(PutMcpCredentialRequestSchema)` to create a new message. + */ +export const PutMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 60); + +/** + * @generated from message engram.app.v1.McpCredentialStatus + */ +export type McpCredentialStatus = Message<"engram.app.v1.McpCredentialStatus"> & { + /** + * @generated from field: string status = 1; + */ + status: string; + + /** + * @generated from field: string reference = 2; + */ + reference: string; + + /** + * @generated from field: string kind = 3; + */ + kind: string; + + /** + * @generated from field: string credential_identity = 4; + */ + credentialIdentity: string; + + /** + * @generated from field: int64 version = 5; + */ + version: bigint; + + /** + * @generated from field: int64 reservation_generation = 6; + */ + reservationGeneration: bigint; +}; + +/** + * Describes the message engram.app.v1.McpCredentialStatus. + * Use `create(McpCredentialStatusSchema)` to create a new message. + */ +export const McpCredentialStatusSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 61); + +/** + * @generated from message engram.app.v1.McpCredentialResponse + */ +export type McpCredentialResponse = Message<"engram.app.v1.McpCredentialResponse"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; + + /** + * @generated from field: engram.app.v1.McpCredentialStatus credential = 2; + */ + credential?: McpCredentialStatus; +}; + +/** + * Describes the message engram.app.v1.McpCredentialResponse. + * Use `create(McpCredentialResponseSchema)` to create a new message. + */ +export const McpCredentialResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 62); + +/** + * @generated from message engram.app.v1.McpApprovedDestination + */ +export type McpApprovedDestination = Message<"engram.app.v1.McpApprovedDestination"> & { + /** + * @generated from field: string zone_id = 1; + */ + zoneId: string; + + /** + * @generated from field: string endpoint = 2; + */ + endpoint: string; +}; + +/** + * Describes the message engram.app.v1.McpApprovedDestination. + * Use `create(McpApprovedDestinationSchema)` to create a new message. + */ +export const McpApprovedDestinationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 63); + +/** + * @generated from message engram.app.v1.McpClientRegistration + */ +export type McpClientRegistration = Message<"engram.app.v1.McpClientRegistration"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string client_id = 2; + */ + clientId: string; + + /** + * @generated from field: bool confidential = 3; + */ + confidential: boolean; + + /** + * @generated from field: string redirect_uri = 4; + */ + redirectUri: string; + + /** + * @generated from field: string issuer = 5; + */ + issuer: string; + + /** + * @generated from field: engram.app.v1.McpApprovedDestination resource = 6; + */ + resource?: McpApprovedDestination; + + /** + * @generated from field: repeated string scopes = 7; + */ + scopes: string[]; +}; + +/** + * Describes the message engram.app.v1.McpClientRegistration. + * Use `create(McpClientRegistrationSchema)` to create a new message. + */ +export const McpClientRegistrationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 64); + +/** + * @generated from message engram.app.v1.GetMcpSetupRequest + */ +export type GetMcpSetupRequest = Message<"engram.app.v1.GetMcpSetupRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; +}; + +/** + * Describes the message engram.app.v1.GetMcpSetupRequest. + * Use `create(GetMcpSetupRequestSchema)` to create a new message. + */ +export const GetMcpSetupRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 65); + +/** + * @generated from message engram.app.v1.GetMcpSetupResponse + */ +export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & { + /** + * @generated from field: bool enabled = 1; + */ + enabled: boolean; + + /** + * @generated from field: repeated engram.app.v1.McpClientRegistration registrations = 2; + */ + registrations: McpClientRegistration[]; + + /** + * @generated from field: repeated engram.app.v1.McpApprovedDestination destinations = 3; + */ + destinations: McpApprovedDestination[]; + + /** + * @generated from field: engram.app.v1.McpCredentialResponse current = 4; + */ + current?: McpCredentialResponse; + + /** + * @generated from field: bool transition_pending = 5; + */ + transitionPending: boolean; + + /** + * @generated from field: string flow_id = 6; + */ + flowId: string; + + /** + * @generated from field: string flow_expires_at = 7; + */ + flowExpiresAt: string; + + /** + * @generated from field: bool client_secret_configured = 8; + */ + clientSecretConfigured: boolean; +}; + +/** + * Describes the message engram.app.v1.GetMcpSetupResponse. + * Use `create(GetMcpSetupResponseSchema)` to create a new message. + */ +export const GetMcpSetupResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 66); + +/** + * @generated from message engram.app.v1.CompleteMcpAuthorizationRequest + */ +export type CompleteMcpAuthorizationRequest = Message<"engram.app.v1.CompleteMcpAuthorizationRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; + + /** + * @generated from field: uint32 expected_generation = 3; + */ + expectedGeneration: number; + + /** + * @generated from field: string flow_id = 4; + */ + flowId: string; + + /** + * @generated from field: string state = 5; + */ + state: string; + + /** + * @generated from field: string code = 6; + */ + code: string; + + /** + * @generated from field: optional string issuer = 7; + */ + issuer?: string; + + /** + * @generated from field: string redirect_uri = 8; + */ + redirectUri: string; +}; + +/** + * Describes the message engram.app.v1.CompleteMcpAuthorizationRequest. + * Use `create(CompleteMcpAuthorizationRequestSchema)` to create a new message. + */ +export const CompleteMcpAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 67); + +/** + * @generated from message engram.app.v1.RecoverMcpCredentialRequest + */ +export type RecoverMcpCredentialRequest = Message<"engram.app.v1.RecoverMcpCredentialRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: uint32 expected_revision = 2; + */ + expectedRevision: number; + + /** + * @generated from field: uint32 expected_generation = 3; + */ + expectedGeneration: number; +}; + +/** + * Describes the message engram.app.v1.RecoverMcpCredentialRequest. + * Use `create(RecoverMcpCredentialRequestSchema)` to create a new message. + */ +export const RecoverMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 68); + /** * ADR 0057 C3: orchestrator-native connector catalog CRUD (Plane B). Connectors * live in the orchestrator DB (the `connector` table, C1); the built-ins @@ -1826,6 +2196,38 @@ export const IntegrationService: GenService<{ input: typeof PublishMcpRequestSchema; output: typeof PublishMcpResponseSchema; }, + /** + * @generated from rpc engram.app.v1.IntegrationService.PutMcpCredential + */ + putMcpCredential: { + methodKind: "unary"; + input: typeof PutMcpCredentialRequestSchema; + output: typeof McpCredentialResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.GetMcpSetup + */ + getMcpSetup: { + methodKind: "unary"; + input: typeof GetMcpSetupRequestSchema; + output: typeof GetMcpSetupResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.CompleteMcpAuthorization + */ + completeMcpAuthorization: { + methodKind: "unary"; + input: typeof CompleteMcpAuthorizationRequestSchema; + output: typeof McpCredentialResponseSchema; + }, + /** + * @generated from rpc engram.app.v1.IntegrationService.RecoverMcpCredential + */ + recoverMcpCredential: { + methodKind: "unary"; + input: typeof RecoverMcpCredentialRequestSchema; + output: typeof McpCredentialResponseSchema; + }, /** * @generated from rpc engram.app.v1.IntegrationService.AuthorizeMcp */ From f1aaa99859c67b122662ea4c24e2084bb2cf9a79 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 00:32:09 -0500 Subject: [PATCH 015/279] fix(orchestrator): exclude query values from HTTP access logs --- orchestrator/src/__tests__/http-log.test.ts | 38 +++++++++++++++++++++ orchestrator/src/http-log.ts | 23 +++++++++++++ orchestrator/src/index.ts | 7 ++-- 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 orchestrator/src/__tests__/http-log.test.ts create mode 100644 orchestrator/src/http-log.ts diff --git a/orchestrator/src/__tests__/http-log.test.ts b/orchestrator/src/__tests__/http-log.test.ts new file mode 100644 index 000000000..fe00a1915 --- /dev/null +++ b/orchestrator/src/__tests__/http-log.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test"; +import { Hono } from "hono"; +import { makeHttpLog } from "../http-log.ts"; +test("HTTP logs retain method path status and time without callback query or request body", async () => { + const records: unknown[] = []; + let now = 100; + const app = new Hono(); + app.use( + makeHttpLog( + (record) => records.push(record), + () => now, + ), + ); + app.get("/oauth/mcp", (c) => { + now = 125; + return c.text("login required", 401); + }); + app.post("/credential", (c) => c.text("ok")); + await app.request( + "https://app.example.test/oauth/mcp?code=secret-code&state=secret-state&iss=https://issuer.example", + ); + await app.request("https://app.example.test/credential", { + method: "POST", + headers: { authorization: "Bearer private" }, + body: "entered-secret", + }); + expect(records[0]).toEqual({ method: "GET", path: "/oauth/mcp", status: 401, durationMs: 25 }); + expect(records[1]).toEqual({ method: "POST", path: "/credential", status: 200, durationMs: 0 }); + const text = JSON.stringify(records); + for (const secret of [ + "secret-code", + "secret-state", + "issuer.example", + "entered-secret", + "private", + ]) + expect(text).not.toContain(secret); +}); diff --git a/orchestrator/src/http-log.ts b/orchestrator/src/http-log.ts new file mode 100644 index 000000000..76daf7d2d --- /dev/null +++ b/orchestrator/src/http-log.ts @@ -0,0 +1,23 @@ +import type { MiddlewareHandler } from "hono"; +export interface HttpLogRecord { + method: string; + path: string; + status: number; + durationMs: number; +} +/** Query strings can contain authorization codes. Never log URLs or bodies. */ +export function makeHttpLog( + write: (record: HttpLogRecord) => void, + now: () => number = Date.now, +): MiddlewareHandler { + return async (context, next) => { + const start = now(); + await next(); + write({ + method: context.req.method, + path: context.req.path, + status: context.res.status, + durationMs: Math.max(0, now() - start), + }); + }; +} diff --git a/orchestrator/src/index.ts b/orchestrator/src/index.ts index f964f5c50..c2285d4d2 100644 --- a/orchestrator/src/index.ts +++ b/orchestrator/src/index.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { Hono } from "hono"; -import { logger as honoLogger } from "hono/logger"; +import { makeHttpLog } from "./http-log.ts"; import { config } from "./config.ts"; import { log } from "./log.ts"; import { buildServer } from "./server.ts"; @@ -267,10 +267,9 @@ const specSyncHub = new SpecSyncHub({ }); setSpecPresence(specSyncHub); -// Request logging (hono/logger) routed through pino, so every HTTP leg — the -// Slack webhooks included — logs `<-- METHOD path` / `--> METHOD path status ms`. +// Request metadata only. OAuth codes and state in query strings are not logged. const httpLog = log.child({ component: "http" }); -app.use(honoLogger((message) => httpLog.info(message))); +app.use(makeHttpLog((record) => httpLog.info(record, "HTTP request"))); // ADR 0064 P2b: live-host preview reverse-proxy. Mounted FIRST so a request to // `.` is resolved + tunneled to the guest port before From 3df868b28a883b58f34ac77a07a9f4a14c79b674 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 00:52:07 -0500 Subject: [PATCH 016/279] fix(mcp): fence canceled OAuth attempts before recovery --- crates/engram-coordinator/src/mcp/control.rs | 22 ++ crates/engram-coordinator/src/mcp/oauth.rs | 15 +- .../engram-coordinator/tests/mcp_control.rs | 61 ++++- crates/engram-coordinator/tests/mcp_oauth.rs | 89 +++++++ crates/engram-core/src/traits/metadata.rs | 20 ++ crates/engram-postgres/src/lib.rs | 189 ++++++++++----- .../proto/engram/mcp/v1/control.proto | 16 ++ crates/engram-sim/src/meta/store_impl.rs | 121 +++++++--- crates/engram-sim/tests/meta_conformance.rs | 224 ++++++++++++++++++ docs/adr/0125-isolated-external-mcp.md | 13 + .../__tests__/mcp-credential-oauth.test.ts | 195 ++++++++++++++- .../src/gen/engram/mcp/v1/control_pb.ts | 81 ++++++- orchestrator/src/mcp/credential-admin.ts | 57 ++++- orchestrator/src/mcp/credential-store.ts | 3 +- .../control-CredentialControl_connectquery.ts | 5 + web/src/gen/engram/mcp/v1/control_pb.ts | 81 ++++++- 16 files changed, 1083 insertions(+), 109 deletions(-) diff --git a/crates/engram-coordinator/src/mcp/control.rs b/crates/engram-coordinator/src/mcp/control.rs index 857625de7..a6999f024 100644 --- a/crates/engram-coordinator/src/mcp/control.rs +++ b/crates/engram-coordinator/src/mcp/control.rs @@ -322,6 +322,28 @@ impl CredentialControl for Control { self.authority.callback(&callback).await.map_err(error)?, ))) } + async fn cancel_authorization( + &self, + req: Request, + ) -> RpcResult { + let _permit = self.check(&req, req.get_ref().protocol)?; + let v = req.into_inner(); + actor(&v.actor_id)?; + let id = v + .flow_id + .parse() + .map_err(|_| Status::invalid_argument("invalid flow id"))?; + self.authority + .cancel_authorization(id, &v.connection_id, &v.registration_id) + .await + .map_err(error)?; + Ok(Response::new(wire::CancelAuthorizationResponse { + protocol: wire::PROTOCOL, + flow_id: v.flow_id, + connection_id: v.connection_id, + registration_id: v.registration_id, + })) + } async fn get_status( &self, req: Request, diff --git a/crates/engram-coordinator/src/mcp/oauth.rs b/crates/engram-coordinator/src/mcp/oauth.rs index 66c0fab18..3b6916932 100644 --- a/crates/engram-coordinator/src/mcp/oauth.rs +++ b/crates/engram-coordinator/src/mcp/oauth.rs @@ -266,6 +266,19 @@ impl McpAuthority { expires_at, }) } + pub async fn cancel_authorization( + &self, + flow_id: Uuid, + connection_id: &str, + registration_id: &str, + ) -> Result<()> { + self.enabled()?; + self.manager + .meta + .cancel_mcp_oauth_flow(flow_id, &key(connection_id, registration_id)) + .await?; + Ok(()) + } pub async fn callback(&self, callback: &Callback) -> Result { self.enabled()?; // The winner removes ciphertext atomically BEFORE any token request. @@ -338,7 +351,7 @@ impl McpAuthority { let row = self .manager .meta - .put_oauth_credential(sealed, flow.expected_version) + .complete_mcp_oauth_flow(callback.flow_id, sealed, flow.expected_version) .await?; Ok(status(&bundle, &row)) } diff --git a/crates/engram-coordinator/tests/mcp_control.rs b/crates/engram-coordinator/tests/mcp_control.rs index 51d189266..a9c0b3f22 100644 --- a/crates/engram-coordinator/tests/mcp_control.rs +++ b/crates/engram-coordinator/tests/mcp_control.rs @@ -2,7 +2,10 @@ use engram_coordinator::{ mcp::{control::*, model::Registry, *}, oauth::OAuthManager, }; -use engram_core::traits::StaticSecretStore; +use engram_core::{ + traits::{Clock, MetadataStore, StaticSecretStore}, + types::{mcp::McpSealedState, oauth::*}, +}; use engram_crypto::EnvVarKeyProvider; use engram_protocol::mcp as wire; use engram_sim::{ManualClock, SimEntropy, SimMetadataStore}; @@ -34,9 +37,9 @@ async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() let entropy = Arc::new(SimEntropy::seeded(1)); let meta = SimMetadataStore::new(clock.clone(), entropy.clone()); let manager = OAuthManager::new( - meta, + meta.clone(), Arc::new(EnvVarKeyProvider::from_bytes([7; 32], "test")), - clock, + clock.clone(), entropy, Arc::new(StaticSecretStore::new(HashMap::new())), ); @@ -143,6 +146,58 @@ async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() .await .unwrap_err(); assert_eq!(missing.code(), tonic::Code::NotFound); + let id = uuid::Uuid::from_u128(999); + let now = clock.now_utc(); + meta.create_mcp_oauth_flow( + OAuthFlow { + id, + key: OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: "connection-a".into(), + provider: "registered-a".into(), + }, + owner_replica: "replica-a".into(), + lease_expires_at: now + chrono::Duration::minutes(10), + expires_at: now + chrono::Duration::minutes(10), + status: OAuthFlowStatus::Pending, + error_code: None, + created_at: now, + updated_at: now, + }, + McpSealedState { + wrapped_dek: vec![], + nonce: vec![], + ciphertext: vec![], + key_id: "test".into(), + }, + ) + .await + .unwrap(); + let cancel = wire::CancelAuthorizationRequest { + protocol: 1, + flow_id: id.to_string(), + connection_id: "connection-a".into(), + registration_id: "registered-a".into(), + actor_id: "admin".into(), + }; + for _ in 0..2 { + let receipt = client + .cancel_authorization(cancel.clone()) + .await + .unwrap() + .into_inner(); + assert_eq!(receipt.flow_id, cancel.flow_id); + assert_eq!(receipt.connection_id, cancel.connection_id); + assert_eq!(receipt.registration_id, cancel.registration_id); + assert_eq!(receipt.protocol, 1); + } + assert_eq!( + meta.get_oauth_flow(id).await.unwrap().unwrap().status, + OAuthFlowStatus::Cancelled + ); + let mut wrong = cancel; + wrong.connection_id = "unrelated".into(); + assert!(client.cancel_authorization(wrong).await.is_err()); } let error = client .get_status(wire::GetStatusRequest { diff --git a/crates/engram-coordinator/tests/mcp_oauth.rs b/crates/engram-coordinator/tests/mcp_oauth.rs index 3b6534f38..99d60ff50 100644 --- a/crates/engram-coordinator/tests/mcp_oauth.rs +++ b/crates/engram-coordinator/tests/mcp_oauth.rs @@ -22,10 +22,15 @@ impl SecurityEntropy for Security { Ok([self.0.fetch_add(1, Ordering::SeqCst); 32]) } } +struct TokenPause { + entered: tokio::sync::oneshot::Sender<()>, + resume: tokio::sync::oneshot::Receiver<()>, +} struct Provider { identity: WorkerIdentity, registration: ClientRegistration, grants: Mutex>, + pause: Mutex>, calls: Mutex>, } #[async_trait::async_trait] @@ -55,6 +60,11 @@ impl WorkerTransport for Provider { ), WorkerOperation::ValidateDestination { .. } => Ok(json!({"valid":true})), WorkerOperation::Token { .. } => { + let pause = self.pause.lock().unwrap().take(); + if let Some(pause) = pause { + pause.entered.send(()).unwrap(); + pause.resume.await.unwrap(); + } self.grants.lock().unwrap().pop().ok_or(McpError::Transport) } } @@ -135,6 +145,7 @@ fn fixture_with_client(grants: Vec, confidential: bool) -> Fixture { identity, registration: registration.clone(), grants: Mutex::new(grants), + pause: Mutex::new(None), calls: Mutex::new(vec![]), }); let security = Arc::new(Security(AtomicU8::new(1))); @@ -597,3 +608,81 @@ async fn revoke_before_authority_switch_fences_old_leases_and_late_callbacks() { f.authority.callback(&callback(&flow)).await.unwrap(); assert!(f.authority.lease(&old_key_lease).await.is_err()); } + +#[tokio::test] +async fn cancelled_attempt_cannot_exchange_or_block_replacement() { + for reconnect in [false, true] { + let f = fixture(vec![grant("new"), grant("old")]); + let expected = if reconnect { + let first = begin(&f).await; + let status = f.authority.callback(&callback(&first)).await.unwrap(); + Some(f.authority.revoke(&status).await.unwrap().version) + } else { + None + }; + let pending = f + .authority + .begin(&f.change, "registered-a", None, expected, "actor-a", true) + .await + .unwrap(); + let before = f.provider.calls.lock().unwrap().len(); + f.replica + .cancel_authorization(pending.flow_id, &f.change.connection_id, "registered-a") + .await + .unwrap(); + assert!(f.authority.callback(&callback(&pending)).await.is_err()); + assert_eq!(f.provider.calls.lock().unwrap().len(), before); + let replacement = f + .replica + .begin(&f.change, "registered-a", None, expected, "actor-a", true) + .await + .unwrap(); + f.authority + .cancel_authorization(pending.flow_id, &f.change.connection_id, "registered-a") + .await + .unwrap(); + let published = f.replica.callback(&callback(&replacement)).await.unwrap(); + assert!(f.authority.callback(&callback(&pending)).await.is_err()); + assert_eq!( + f.authority + .status(&f.change.connection_id, &published.reference) + .await + .unwrap(), + published + ); + } +} + +#[tokio::test] +async fn cancellation_fences_an_inflight_token_response() { + let f = fixture(vec![grant("late")]); + let begun = begin(&f).await; + let callback = callback(&begun); + let (entered, wait) = tokio::sync::oneshot::channel(); + let (resume, paused) = tokio::sync::oneshot::channel(); + *f.provider.pause.lock().unwrap() = Some(TokenPause { + entered, + resume: paused, + }); + let complete = f.authority.callback(&callback); + let cancel = async { + wait.await.unwrap(); + f.replica + .cancel_authorization(begun.flow_id, &f.change.connection_id, "registered-a") + .await + .unwrap(); + resume.send(()).unwrap(); + }; + let (result, ()) = tokio::join!(complete, cancel); + assert!(result.is_err()); + assert!(f + .authority + .status( + &f.change.connection_id, + &CredentialReference::OAuth { + registration_id: "registered-a".into() + } + ) + .await + .is_err()); +} diff --git a/crates/engram-core/src/traits/metadata.rs b/crates/engram-core/src/traits/metadata.rs index 2d1feae83..a021463d3 100644 --- a/crates/engram-core/src/traits/metadata.rs +++ b/crates/engram-core/src/traits/metadata.rs @@ -628,6 +628,26 @@ pub trait MetadataStore: Send + Sync { Err(MetaError::Conflict("MCP flow storage unavailable".into())) } + /// Fence this exact MCP attempt, including consumed state. Terminal attempts + /// are idempotent; a missing or differently scoped attempt is not evidence. + async fn cancel_mcp_oauth_flow( + &self, + _id: uuid::Uuid, + _key: &crate::types::oauth::OAuthCredentialKey, + ) -> Result<(), MetaError> { + Err(MetaError::Conflict("MCP flow storage unavailable".into())) + } + /// Atomically require a live consumed flow, publish by credential version CAS, + /// and finish that flow. Cancellation and publication lock the same flow row. + async fn complete_mcp_oauth_flow( + &self, + _id: uuid::Uuid, + _credential: crate::types::oauth::NewSealedOAuthCredential, + _expected_version: Option, + ) -> Result { + Err(MetaError::Conflict("MCP flow storage unavailable".into())) + } + async fn create_oauth_flow( &self, _flow: crate::types::oauth::OAuthFlow, diff --git a/crates/engram-postgres/src/lib.rs b/crates/engram-postgres/src/lib.rs index e26dae0d7..26c66d61c 100644 --- a/crates/engram-postgres/src/lib.rs +++ b/crates/engram-postgres/src/lib.rs @@ -2088,71 +2088,14 @@ impl MetadataStore for PostgresStore { credential: engram_core::types::oauth::NewSealedOAuthCredential, expected_version: Option, ) -> Result { - let now = self.clock.now_utc(); - let metadata = serde_json::to_value(&credential.metadata) - .map_err(|e| MetaError::Serialization(e.to_string()))?; - let row = if let Some(expected) = expected_version { - sqlx::query( - r#" - UPDATE oauth_credentials SET - wrapped_dek = $4, nonce = $5, ciphertext = $6, key_id = $7, - account_metadata = $8, version = version + 1, - updated_at = $9, revoked_at = NULL, expires_at = $11, - refresh_claim_until = NULL, broken_at = NULL, broken_reason = NULL - WHERE subject_kind = $1 AND subject_id = $2 AND provider = $3 - AND version = $10 - RETURNING * - "#, - ) - .bind(credential.key.subject_kind.as_str()) - .bind(&credential.key.subject_id) - .bind(&credential.key.provider) - .bind(&credential.wrapped_dek) - .bind(&credential.nonce) - .bind(&credential.ciphertext) - .bind(&credential.key_id) - .bind(metadata) - .bind(now) - .bind(expected) - .bind(credential.expires_at) - .fetch_optional(&self.pool) - .await - .map_err(db_err)? - } else { - sqlx::query( - r#" - INSERT INTO oauth_credentials ( - subject_kind, subject_id, provider, wrapped_dek, nonce, - ciphertext, key_id, account_metadata, version, created_at, - updated_at, revoked_at, expires_at - ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,1,$9,$9,NULL,$10) - ON CONFLICT (subject_kind, subject_id, provider) DO NOTHING - RETURNING * - "#, - ) - .bind(credential.key.subject_kind.as_str()) - .bind(&credential.key.subject_id) - .bind(&credential.key.provider) - .bind(&credential.wrapped_dek) - .bind(&credential.nonce) - .bind(&credential.ciphertext) - .bind(&credential.key_id) - .bind(metadata) - .bind(now) - .bind(credential.expires_at) - .fetch_optional(&self.pool) - .await - .map_err(db_err)? - }; - match row { - Some(row) => oauth_credential_from_pg(&row), - None => Err(MetaError::Conflict(format!( - "OAuth credential CAS rejected for {}/{}/{}", - credential.key.subject_kind.as_str(), - credential.key.subject_id, - credential.key.provider - ))), - } + let mut conn = self.pool.acquire().await.map_err(db_err)?; + put_oauth_credential_on( + &mut conn, + self.clock.now_utc(), + credential, + expected_version, + ) + .await } async fn get_oauth_credential( @@ -2347,6 +2290,50 @@ impl MetadataStore for PostgresStore { serde_json::from_value(state).map_err(|e| MetaError::Serialization(e.to_string())) } + async fn cancel_mcp_oauth_flow( + &self, + id: uuid::Uuid, + key: &engram_core::types::oauth::OAuthCredentialKey, + ) -> Result<(), MetaError> { + if key.subject_kind != engram_core::types::oauth::OAuthSubjectKind::Mcp { + return Err(MetaError::Conflict("MCP flow cancellation rejected".into())); + } + let result = sqlx::query("UPDATE oauth_flows SET status=CASE WHEN status='pending' THEN 'cancelled' ELSE status END, mcp_state=NULL, updated_at=$4 WHERE id=$1 AND subject_kind='mcp' AND subject_id=$2 AND provider=$3") + .bind(id).bind(&key.subject_id).bind(&key.provider).bind(self.clock.now_utc()) + .execute(&self.pool).await.map_err(db_err)?; + if result.rows_affected() != 1 { + return Err(MetaError::Conflict("MCP flow cancellation rejected".into())); + } + Ok(()) + } + async fn complete_mcp_oauth_flow( + &self, + id: uuid::Uuid, + credential: engram_core::types::oauth::NewSealedOAuthCredential, + expected_version: Option, + ) -> Result { + if credential.key.subject_kind != engram_core::types::oauth::OAuthSubjectKind::Mcp { + return Err(MetaError::Conflict("MCP flow publication rejected".into())); + } + let mut tx = self.pool.begin().await.map_err(db_err)?; + let now = self.clock.now_utc(); + let locked: Option = sqlx::query_scalar("SELECT id FROM oauth_flows WHERE id=$1 AND subject_kind='mcp' AND subject_id=$2 AND provider=$3 AND status='pending' AND mcp_state IS NULL AND expires_at>$4 FOR UPDATE") + .bind(id).bind(&credential.key.subject_id).bind(&credential.key.provider).bind(now) + .fetch_optional(&mut *tx).await.map_err(db_err)?; + if locked.is_none() { + return Err(MetaError::Conflict("MCP flow publication rejected".into())); + } + let row = put_oauth_credential_on(&mut tx, now, credential, expected_version).await?; + sqlx::query("UPDATE oauth_flows SET status='succeeded', updated_at=$2 WHERE id=$1") + .bind(id) + .bind(now) + .execute(&mut *tx) + .await + .map_err(db_err)?; + tx.commit().await.map_err(db_err)?; + Ok(row) + } + async fn create_oauth_flow( &self, flow: engram_core::types::oauth::OAuthFlow, @@ -10130,3 +10117,75 @@ async fn append_teleport_finished_tx( } Ok(()) } + +async fn put_oauth_credential_on( + conn: &mut sqlx::PgConnection, + now: chrono::DateTime, + credential: engram_core::types::oauth::NewSealedOAuthCredential, + expected_version: Option, +) -> Result { + let metadata = serde_json::to_value(&credential.metadata) + .map_err(|e| MetaError::Serialization(e.to_string()))?; + let row = if let Some(expected) = expected_version { + sqlx::query( + r#" + UPDATE oauth_credentials SET + wrapped_dek = $4, nonce = $5, ciphertext = $6, key_id = $7, + account_metadata = $8, version = version + 1, + updated_at = $9, revoked_at = NULL, expires_at = $11, + refresh_claim_until = NULL, broken_at = NULL, broken_reason = NULL + WHERE subject_kind = $1 AND subject_id = $2 AND provider = $3 + AND version = $10 + RETURNING * + "#, + ) + .bind(credential.key.subject_kind.as_str()) + .bind(&credential.key.subject_id) + .bind(&credential.key.provider) + .bind(&credential.wrapped_dek) + .bind(&credential.nonce) + .bind(&credential.ciphertext) + .bind(&credential.key_id) + .bind(metadata) + .bind(now) + .bind(expected) + .bind(credential.expires_at) + .fetch_optional(&mut *conn) + .await + .map_err(db_err)? + } else { + sqlx::query( + r#" + INSERT INTO oauth_credentials ( + subject_kind, subject_id, provider, wrapped_dek, nonce, + ciphertext, key_id, account_metadata, version, created_at, + updated_at, revoked_at, expires_at + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,1,$9,$9,NULL,$10) + ON CONFLICT (subject_kind, subject_id, provider) DO NOTHING + RETURNING * + "#, + ) + .bind(credential.key.subject_kind.as_str()) + .bind(&credential.key.subject_id) + .bind(&credential.key.provider) + .bind(&credential.wrapped_dek) + .bind(&credential.nonce) + .bind(&credential.ciphertext) + .bind(&credential.key_id) + .bind(metadata) + .bind(now) + .bind(credential.expires_at) + .fetch_optional(&mut *conn) + .await + .map_err(db_err)? + }; + match row { + Some(row) => oauth_credential_from_pg(&row), + None => Err(MetaError::Conflict(format!( + "OAuth credential CAS rejected for {}/{}/{}", + credential.key.subject_kind.as_str(), + credential.key.subject_id, + credential.key.provider + ))), + } +} diff --git a/crates/engram-protocol/proto/engram/mcp/v1/control.proto b/crates/engram-protocol/proto/engram/mcp/v1/control.proto index 5d8149b89..1f1918b81 100644 --- a/crates/engram-protocol/proto/engram/mcp/v1/control.proto +++ b/crates/engram-protocol/proto/engram/mcp/v1/control.proto @@ -5,6 +5,7 @@ package engram.mcp.v1; service CredentialControl { rpc PutSecret(PutSecretRequest) returns (CredentialStatus); rpc BeginAuthorization(BeginAuthorizationRequest) returns (BeginAuthorizationResponse); + rpc CancelAuthorization(CancelAuthorizationRequest) returns (CancelAuthorizationResponse); rpc CompleteAuthorization(CompleteAuthorizationRequest) returns (CredentialStatus); rpc GetStatus(GetStatusRequest) returns (CredentialStatus); rpc Revoke(RevokeRequest) returns (CredentialStatus); @@ -146,3 +147,18 @@ message LeaseResponse { string intent_id = 5; int64 operation_expires_at = 6; } + +// Exact attempt only. Cancellation persists in the existing flow row. +message CancelAuthorizationRequest { + uint32 protocol = 1; + string flow_id = 2; + string connection_id = 3; + string registration_id = 4; + string actor_id = 5; +} +message CancelAuthorizationResponse { + uint32 protocol = 1; + string flow_id = 2; + string connection_id = 3; + string registration_id = 4; +} diff --git a/crates/engram-sim/src/meta/store_impl.rs b/crates/engram-sim/src/meta/store_impl.rs index 04b4b48b2..bb449da3a 100644 --- a/crates/engram-sim/src/meta/store_impl.rs +++ b/crates/engram-sim/src/meta/store_impl.rs @@ -5660,35 +5660,12 @@ impl MetadataStore for SimMetadataStore { expected_version: Option, ) -> Result { self.gate()?; - let now = self.now(); - let mut db = self.db.lock(); - let (version, created_at) = match db.oauth_credentials.get(&credential.key) { - None if expected_version.is_none() => (1, now), - Some(current) if expected_version == Some(current.version) => { - (current.version + 1, current.created_at) - } - _ => return Err(MetaError::Conflict("OAuth credential CAS rejected".into())), - }; - let row = engram_core::types::oauth::SealedOAuthCredential { - key: credential.key.clone(), - wrapped_dek: credential.wrapped_dek, - nonce: credential.nonce, - ciphertext: credential.ciphertext, - key_id: credential.key_id, - metadata: credential.metadata, - version, - created_at, - updated_at: now, - revoked_at: None, - expires_at: credential.expires_at, - broken_at: None, - broken_reason: None, - }; - // Publishing a validated bundle IS the repair: broken/claim state - // clears on every successful write (PG mirrors this in the UPDATE). - db.oauth_refresh_claims.remove(&credential.key); - db.oauth_credentials.insert(credential.key, row.clone()); - Ok(row) + put_oauth_credential_in( + &mut self.db.lock(), + self.now(), + credential, + expected_version, + ) } async fn list_oauth_credentials_due_for_refresh( @@ -5946,6 +5923,57 @@ impl MetadataStore for SimMetadataStore { .ok_or_else(|| MetaError::Conflict("MCP state consumed".into())) } + async fn cancel_mcp_oauth_flow( + &self, + id: uuid::Uuid, + key: &engram_core::types::oauth::OAuthCredentialKey, + ) -> Result<(), MetaError> { + use engram_core::types::oauth::{OAuthFlowStatus, OAuthSubjectKind}; + self.gate()?; + let mut db = self.db.lock(); + let flow = db + .oauth_flows + .get_mut(&id) + .ok_or_else(|| MetaError::Conflict("MCP flow cancellation rejected".into()))?; + if key.subject_kind != OAuthSubjectKind::Mcp || &flow.key != key { + return Err(MetaError::Conflict("MCP flow cancellation rejected".into())); + } + if flow.status == OAuthFlowStatus::Pending { + flow.status = OAuthFlowStatus::Cancelled; + } + flow.updated_at = self.now(); + db.mcp_flow_states.remove(&id); + Ok(()) + } + async fn complete_mcp_oauth_flow( + &self, + id: uuid::Uuid, + credential: engram_core::types::oauth::NewSealedOAuthCredential, + expected_version: Option, + ) -> Result { + use engram_core::types::oauth::{OAuthFlowStatus, OAuthSubjectKind}; + self.gate()?; + let mut db = self.db.lock(); + let now = self.now(); + let flow = db + .oauth_flows + .get(&id) + .ok_or_else(|| MetaError::Conflict("MCP flow publication rejected".into()))?; + if credential.key.subject_kind != OAuthSubjectKind::Mcp + || flow.key != credential.key + || flow.status != OAuthFlowStatus::Pending + || flow.expires_at <= now + || db.mcp_flow_states.contains_key(&id) + { + return Err(MetaError::Conflict("MCP flow publication rejected".into())); + } + let row = put_oauth_credential_in(&mut db, now, credential, expected_version)?; + let flow = db.oauth_flows.get_mut(&id).expect("locked flow"); + flow.status = OAuthFlowStatus::Succeeded; + flow.updated_at = now; + Ok(row) + } + async fn create_oauth_flow( &self, flow: engram_core::types::oauth::OAuthFlow, @@ -6173,3 +6201,38 @@ fn append_event_idempotent_locked( }); Ok(Some(idx)) } + +fn put_oauth_credential_in( + db: &mut SimDb, + now: DateTime, + credential: engram_core::types::oauth::NewSealedOAuthCredential, + expected_version: Option, +) -> Result { + let (version, created_at) = match db.oauth_credentials.get(&credential.key) { + None if expected_version.is_none() => (1, now), + Some(current) if expected_version == Some(current.version) => { + (current.version + 1, current.created_at) + } + _ => return Err(MetaError::Conflict("OAuth credential CAS rejected".into())), + }; + let row = engram_core::types::oauth::SealedOAuthCredential { + key: credential.key.clone(), + wrapped_dek: credential.wrapped_dek, + nonce: credential.nonce, + ciphertext: credential.ciphertext, + key_id: credential.key_id, + metadata: credential.metadata, + version, + created_at, + updated_at: now, + revoked_at: None, + expires_at: credential.expires_at, + broken_at: None, + broken_reason: None, + }; + // Publishing a validated bundle IS the repair: broken/claim state + // clears on every successful write (PG mirrors this in the UPDATE). + db.oauth_refresh_claims.remove(&credential.key); + db.oauth_credentials.insert(credential.key, row.clone()); + Ok(row) +} diff --git a/crates/engram-sim/tests/meta_conformance.rs b/crates/engram-sim/tests/meta_conformance.rs index 400a85172..86facb4f8 100644 --- a/crates/engram-sim/tests/meta_conformance.rs +++ b/crates/engram-sim/tests/meta_conformance.rs @@ -7820,3 +7820,227 @@ async fn mcp_admission_and_refresh(ctx: &Ctx) { .is_some()); } conformance!(t_mcp_admission_and_refresh, mcp_admission_and_refresh); + +// Cancellation and publication use the same flow row even when no credential exists. +async fn mcp_flow_cancellation_fences_publication(ctx: &Ctx) { + use engram_core::types::{mcp::*, oauth::*}; + let now = ctx.clock.now_utc(); + for reconnect in [false, true] { + let key = OAuthCredentialKey { + subject_kind: OAuthSubjectKind::Mcp, + subject_id: format!("cancel-{reconnect}"), + provider: "registered-a".into(), + }; + let candidate = NewSealedOAuthCredential { + key: key.clone(), + wrapped_dek: vec![1; 32], + nonce: vec![2; 12], + ciphertext: vec![3; 64], + key_id: "test".into(), + metadata: OAuthAccountMetadata::default(), + expires_at: None, + }; + let expected = if reconnect { + let old = ctx + .meta + .put_oauth_credential(candidate.clone(), None) + .await + .unwrap(); + ctx.meta + .revoke_oauth_credential(&key, old.version) + .await + .unwrap(); + Some( + ctx.meta + .get_oauth_credential(&key) + .await + .unwrap() + .unwrap() + .version, + ) + } else { + None + }; + let base = if reconnect { 920 } else { 910 }; + let flow = |n: u128| OAuthFlow { + id: uuid::Uuid::from_u128(base + n), + key: key.clone(), + owner_replica: "replica-a".into(), + lease_expires_at: now + chrono::Duration::minutes(10), + expires_at: now + chrono::Duration::minutes(10), + status: OAuthFlowStatus::Pending, + error_code: None, + created_at: now, + updated_at: now, + }; + let state = McpSealedState { + wrapped_dek: vec![1; 32], + nonce: vec![2; 12], + ciphertext: vec![3; 64], + key_id: "test".into(), + }; + let old = flow(0); + assert!(ctx + .meta + .complete_mcp_oauth_flow(old.id, candidate.clone(), expected) + .await + .is_err()); + ctx.meta + .create_mcp_oauth_flow(old.clone(), state.clone()) + .await + .unwrap(); + // An unknown (lost response) pending attempt blocks same-subject begin. + assert!(ctx + .meta + .create_mcp_oauth_flow(flow(9), state.clone()) + .await + .is_err()); + let mut wrong = key.clone(); + wrong.subject_id = "unrelated".into(); + assert!(ctx + .meta + .cancel_mcp_oauth_flow(old.id, &wrong) + .await + .is_err()); + ctx.meta.cancel_mcp_oauth_flow(old.id, &key).await.unwrap(); + ctx.meta.cancel_mcp_oauth_flow(old.id, &key).await.unwrap(); + assert!(ctx + .meta + .create_mcp_oauth_flow(old.clone(), state.clone()) + .await + .is_err()); + assert!(ctx.meta.consume_mcp_oauth_flow(old.id, &key).await.is_err()); + assert!(ctx + .meta + .complete_mcp_oauth_flow(old.id, candidate.clone(), expected) + .await + .is_err()); + let inflight = flow(1); + ctx.meta + .create_mcp_oauth_flow(inflight.clone(), state.clone()) + .await + .unwrap(); + ctx.meta + .consume_mcp_oauth_flow(inflight.id, &key) + .await + .unwrap(); + ctx.meta + .cancel_mcp_oauth_flow(inflight.id, &key) + .await + .unwrap(); + assert!(ctx + .meta + .complete_mcp_oauth_flow(inflight.id, candidate.clone(), expected) + .await + .is_err()); + // A flow that has not consumed state cannot publish. + let untouched = flow(3); + ctx.meta + .create_mcp_oauth_flow(untouched.clone(), state.clone()) + .await + .unwrap(); + assert!(ctx + .meta + .complete_mcp_oauth_flow(untouched.id, candidate.clone(), expected) + .await + .is_err()); + ctx.meta + .cancel_mcp_oauth_flow(untouched.id, &key) + .await + .unwrap(); + let replacement = flow(2); + ctx.meta + .create_mcp_oauth_flow(replacement.clone(), state) + .await + .unwrap(); + ctx.meta + .consume_mcp_oauth_flow(replacement.id, &key) + .await + .unwrap(); + ctx.meta.cancel_mcp_oauth_flow(old.id, &key).await.unwrap(); + let mut wrong_candidate = candidate.clone(); + wrong_candidate.key.subject_id = "unrelated".into(); + assert!(ctx + .meta + .complete_mcp_oauth_flow(replacement.id, wrong_candidate, expected) + .await + .is_err()); + assert!(ctx + .meta + .complete_mcp_oauth_flow(replacement.id, candidate.clone(), Some(999)) + .await + .is_err()); + let published = ctx + .meta + .complete_mcp_oauth_flow(replacement.id, candidate.clone(), expected) + .await + .unwrap(); + assert!(ctx + .meta + .complete_mcp_oauth_flow(inflight.id, candidate, expected) + .await + .is_err()); + assert_eq!( + ctx.meta.get_oauth_credential(&key).await.unwrap().unwrap(), + published + ); + assert_eq!( + ctx.meta + .get_oauth_flow(replacement.id) + .await + .unwrap() + .unwrap() + .status, + OAuthFlowStatus::Succeeded + ); + // Either transaction may win; a cancellation winner cannot publish. + let race = flow(4); + ctx.meta + .create_mcp_oauth_flow( + race.clone(), + McpSealedState { + wrapped_dek: vec![1; 32], + nonce: vec![2; 12], + ciphertext: vec![3; 64], + key_id: "test".into(), + }, + ) + .await + .unwrap(); + ctx.meta + .consume_mcp_oauth_flow(race.id, &key) + .await + .unwrap(); + let candidate = NewSealedOAuthCredential { + key: key.clone(), + wrapped_dek: vec![4; 32], + nonce: vec![5; 12], + ciphertext: vec![6; 64], + key_id: "test".into(), + metadata: OAuthAccountMetadata::default(), + expires_at: None, + }; + let (cancel, complete) = tokio::join!( + ctx.meta.cancel_mcp_oauth_flow(race.id, &key), + ctx.meta + .complete_mcp_oauth_flow(race.id, candidate, Some(published.version)) + ); + cancel.unwrap(); + let terminal = ctx + .meta + .get_oauth_flow(race.id) + .await + .unwrap() + .unwrap() + .status; + assert_eq!(complete.is_ok(), terminal == OAuthFlowStatus::Succeeded); + assert!(matches!( + terminal, + OAuthFlowStatus::Succeeded | OAuthFlowStatus::Cancelled + )); + } +} +conformance!( + t_mcp_flow_cancellation_fences_publication, + mcp_flow_cancellation_fences_publication +); diff --git a/docs/adr/0125-isolated-external-mcp.md b/docs/adr/0125-isolated-external-mcp.md index d226c4934..f45167392 100644 --- a/docs/adr/0125-isolated-external-mcp.md +++ b/docs/adr/0125-isolated-external-mcp.md @@ -143,6 +143,19 @@ authority revoked. Late control replies and callbacks cannot publish through an old token. A lost fresh-reference response can leave a sealed orphan; recovery cannot publish an unknown reference or restore the previous credential. +A local transition token alone cannot fence a delayed coordinator callback. +Before recovery or replacement, the orchestrator cancels each recorded attempt +by exact flow UUID, MCP connection and registration. Cancellation clears sealed +state and marks the existing flow row cancelled. Callback consumption admits token +I/O; cancellation before consumption prevents that exchange. Credential publication +locks the same flow row, requires live consumed state, applies credential-version +CAS and marks success in one transaction. Cancellation after consumption therefore +fences publication even if external I/O cannot be stopped. If publication wins, +recovery queries and revokes that exact credential before clearing its local fence. +A lost begin response can leave an unknown pending flow until expiry and cleanup; +the stale orchestrator call cannot return its authorization URL or record a usable +callback after recovery. Unknown flow IDs are never inferred or broadly cancelled. + ### Authorization and irreversible dispatch Before execution, validate the active session/run, immutable binding, principal, diff --git a/orchestrator/src/__tests__/mcp-credential-oauth.test.ts b/orchestrator/src/__tests__/mcp-credential-oauth.test.ts index eade52e2b..5910086c0 100644 --- a/orchestrator/src/__tests__/mcp-credential-oauth.test.ts +++ b/orchestrator/src/__tests__/mcp-credential-oauth.test.ts @@ -37,6 +37,11 @@ async function fixture() { lostCallback = false, outage = false; const calls: string[] = []; + const cancelled = new Set(); + let flowNumber = 0; + let pauseComplete: (() => Promise) | undefined; + let pauseBegin: (() => Promise) | undefined; + let pauseStatus: (() => Promise) | undefined; const control = createClient( CredentialControl, createRouterTransport((router) => @@ -46,15 +51,27 @@ async function fixture() { if (!current) throw new ConnectError("absent", Code.NotFound); return current; }, + async cancelAuthorization(req) { + if (outage) throw new ConnectError("outage", Code.Unavailable); + expect(req.connectionId).toBe(row.id); + cancelled.add(req.flowId); + return { + protocol: 1, + flowId: req.flowId, + connectionId: req.connectionId, + registrationId: req.registrationId, + }; + }, async beginAuthorization(req) { calls.push("begin"); expect(req.sharingConsent).toBe(true); expect(req.clientSecret).toBeUndefined(); expect(req.registrationId).toBe("public-client"); expect(req.context!.resource!.zoneId).toBe("internal"); + callback.flowId = `flow-${++flowNumber}`; return { protocol: 1, - flowId: "flow", + flowId: callback.flowId, context: { connectionId: row.id, configRevision: req.context!.configRevision, @@ -69,6 +86,7 @@ async function fixture() { }; }, async completeAuthorization(req) { + if (cancelled.has(req.flowId)) throw new ConnectError("cancelled", Code.Aborted); calls.push("complete"); expect(req.state).toBe("secret-state"); expect(req.code).toBe("secret-code"); @@ -98,7 +116,28 @@ async function fixture() { ); const admin = makeCredentialAdmin({ store, - control, + control: { + ...control, + async getStatus(req, options) { + const wait = pauseStatus; + pauseStatus = undefined; + if (wait) await wait(); + return control.getStatus(req, options); + }, + async beginAuthorization(req, options) { + const response = await control.beginAuthorization(req, options); + const wait = pauseBegin; + pauseBegin = undefined; + if (wait) await wait(); + return response; + }, + async completeAuthorization(req, options) { + const wait = pauseComplete; + pauseComplete = undefined; + if (wait) await wait(); + return control.completeAuthorization(req, options); + }, + }, now: () => new Date(clock), registrations: [ { @@ -126,6 +165,33 @@ async function fixture() { admin, callback, calls, + pauseStatus() { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + pauseStatus = async () => { + entered.resolve(); + await release.promise; + }; + return { entered: entered.promise, resume: release.resolve }; + }, + pauseBegin() { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + pauseBegin = async () => { + entered.resolve(); + await release.promise; + }; + return { entered: entered.promise, resume: release.resolve }; + }, + pause() { + const entered = Promise.withResolvers(); + const release = Promise.withResolvers(); + pauseComplete = async () => { + entered.resolve(); + await release.promise; + }; + return { entered: entered.promise, resume: release.resolve }; + }, lose() { lostCallback = true; }, @@ -136,6 +202,7 @@ async function fixture() { outage = value; }, current: () => current, + cancelled: (id: string) => cancelled.has(id), async cleanup() { await connections.delete(row.id); }, @@ -185,6 +252,7 @@ test.skipIf(!reachable)( }); await expect(f.admin.complete(f.row.id, 2, 2, "admin", f.callback)).rejects.toThrow(); expect(f.calls).toEqual(["begin"]); + expect(f.cancelled(f.callback.flowId)).toBe(true); } finally { await f.cleanup(); } @@ -251,3 +319,126 @@ test.skipIf(!reachable)( } }, ); + +for (const reconnect of [false, true]) { + test.skipIf(!reachable)( + `recovery cancels pre-dispatch callback before kind switch (reconnect=${reconnect})`, + async () => { + const f = await fixture(); + try { + await f.admin.begin(f.row.id, 1, 1, "admin", true); + if (reconnect) { + await f.admin.complete(f.row.id, 2, 2, "admin", f.callback); + await f.admin.begin(f.row.id, 2, 3, "admin", true); + } + const row = await f.store.get(f.row.id); + const pause = f.pause(); + const late = f.admin.complete(row.id, row.configRevision, row.authGeneration, "admin", { + ...f.callback, + }); + const outcome = late.then( + () => false, + () => true, + ); + await pause.entered; + const exchanges = f.calls.filter((c) => c === "complete").length; + f.advance(); + await f.admin.recover(row.id, row.configRevision, row.authGeneration, "admin"); + await f.admin.configure(row.id, row.configRevision, row.authGeneration, "admin", { + endpoint: config.endpoint, + zone: config.zone, + auth: { kind: "api_key", secretRef: "" }, + }); + pause.resume(); + expect(await outcome).toBe(true); + expect(f.calls.filter((c) => c === "complete")).toHaveLength(exchanges); + expect(f.current()?.status).not.toBe("connected"); + let next = await f.store.get(row.id); + await f.admin.configure(row.id, next.configRevision, next.authGeneration, "admin", config); + next = await f.store.get(row.id); + await f.admin.begin(row.id, next.configRevision, next.authGeneration, "admin", true); + next = await f.store.get(row.id); + await f.admin.complete( + row.id, + next.configRevision, + next.authGeneration, + "admin", + f.callback, + ); + expect((await f.store.get(row.id)).revoked).toBe(false); + } finally { + await f.cleanup(); + } + }, + ); +} + +test.skipIf(!reachable)( + "lost begin response cannot return a public authorization URL after recovery", + async () => { + const f = await fixture(); + try { + const pause = f.pauseBegin(); + const begin = f.admin.begin(f.row.id, 1, 1, "admin", true); + const result = begin.then( + (value) => ({ value }), + () => ({ value: undefined }), + ); + await pause.entered; + f.advance(); + await f.admin.recover(f.row.id, 2, 2, "admin"); + await f.admin.configure(f.row.id, 2, 2, "admin", { + endpoint: config.endpoint, + zone: config.zone, + auth: { kind: "api_key", secretRef: "" }, + }); + pause.resume(); + expect((await result).value).toBeUndefined(); + const row = await f.store.get(f.row.id); + expect(row.mcpCredential.flow).toBeUndefined(); + await expect( + f.admin.complete(row.id, row.configRevision, row.authGeneration, "admin", f.callback), + ).rejects.toThrow(); + expect(f.calls).toEqual(["begin"]); + } finally { + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "old recovery cannot revoke a newer same-registration authority", + async () => { + const f = await fixture(); + try { + await f.admin.begin(f.row.id, 1, 1, "admin", true); + const callbackPause = f.pause(); + const oldCallback = f.admin.complete(f.row.id, 2, 2, "admin", { ...f.callback }).then( + () => false, + () => true, + ); + await callbackPause.entered; + f.advance(); + const statusPause = f.pauseStatus(); + const oldRecovery = f.admin.recover(f.row.id, 2, 2, "admin").then( + () => false, + () => true, + ); + await statusPause.entered; + f.advance(); + await f.admin.recover(f.row.id, 2, 2, "admin"); + await f.admin.begin(f.row.id, 2, 2, "admin", true); + await f.admin.complete(f.row.id, 3, 3, "admin", f.callback); + const replacement = structuredClone(f.current()); + statusPause.resume(); + expect(await oldRecovery).toBe(true); + callbackPause.resume(); + expect(await oldCallback).toBe(true); + expect(f.current()).toEqual(replacement); + expect(f.current()!.status).toBe("connected"); + expect(f.calls.filter((c) => c === "revoke")).toHaveLength(0); + } finally { + await f.cleanup(); + } + }, +); diff --git a/orchestrator/src/gen/engram/mcp/v1/control_pb.ts b/orchestrator/src/gen/engram/mcp/v1/control_pb.ts index 00333e847..c7c78e197 100644 --- a/orchestrator/src/gen/engram/mcp/v1/control_pb.ts +++ b/orchestrator/src/gen/engram/mcp/v1/control_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/control.proto. */ export const file_engram_mcp_v1_control: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDKowBCg5DcmVkZW50aWFsS2luZBIfChtDUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIbChdDUkVERU5USUFMX0tJTkRfQVBJX0tFWRABEiEKHUNSRURFTlRJQUxfS0lORF9DTElFTlRfU0VDUkVUEAISGQoVQ1JFREVOVElBTF9LSU5EX09BVVRIEAMykAQKEUNyZWRlbnRpYWxDb250cm9sEk0KCVB1dFNlY3JldBIfLmVuZ3JhbS5tY3AudjEuUHV0U2VjcmV0UmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmUKFUNvbXBsZXRlQXV0aG9yaXphdGlvbhIrLmVuZ3JhbS5tY3AudjEuQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJNCglHZXRTdGF0dXMSHy5lbmdyYW0ubWNwLnYxLkdldFN0YXR1c1JlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSRwoGUmV2b2tlEhwuZW5ncmFtLm1jcC52MS5SZXZva2VSZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEkIKBUxlYXNlEhsuZW5ncmFtLm1jcC52MS5MZWFzZVJlcXVlc3QaHC5lbmdyYW0ubWNwLnYxLkxlYXNlUmVzcG9uc2ViBnByb3RvMw"); + fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDIoEBChpDYW5jZWxBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJEhAKCGFjdG9yX2lkGAUgASgJInAKG0NhbmNlbEF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJKowBCg5DcmVkZW50aWFsS2luZBIfChtDUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIbChdDUkVERU5USUFMX0tJTkRfQVBJX0tFWRABEiEKHUNSRURFTlRJQUxfS0lORF9DTElFTlRfU0VDUkVUEAISGQoVQ1JFREVOVElBTF9LSU5EX09BVVRIEAMy/gQKEUNyZWRlbnRpYWxDb250cm9sEk0KCVB1dFNlY3JldBIfLmVuZ3JhbS5tY3AudjEuUHV0U2VjcmV0UmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmwKE0NhbmNlbEF1dGhvcml6YXRpb24SKS5lbmdyYW0ubWNwLnYxLkNhbmNlbEF1dGhvcml6YXRpb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DYW5jZWxBdXRob3JpemF0aW9uUmVzcG9uc2USZQoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEk0KCUdldFN0YXR1cxIfLmVuZ3JhbS5tY3AudjEuR2V0U3RhdHVzUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJHCgZSZXZva2USHC5lbmdyYW0ubWNwLnYxLlJldm9rZVJlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); /** * @generated from message engram.mcp.v1.Destination @@ -675,6 +675,77 @@ export type LeaseResponse = Message<"engram.mcp.v1.LeaseResponse"> & { export const LeaseResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_control, 16); +/** + * Exact attempt only. Cancellation persists in the existing flow row. + * + * @generated from message engram.mcp.v1.CancelAuthorizationRequest + */ +export type CancelAuthorizationRequest = Message<"engram.mcp.v1.CancelAuthorizationRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string flow_id = 2; + */ + flowId: string; + + /** + * @generated from field: string connection_id = 3; + */ + connectionId: string; + + /** + * @generated from field: string registration_id = 4; + */ + registrationId: string; + + /** + * @generated from field: string actor_id = 5; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.CancelAuthorizationRequest. + * Use `create(CancelAuthorizationRequestSchema)` to create a new message. + */ +export const CancelAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 17); + +/** + * @generated from message engram.mcp.v1.CancelAuthorizationResponse + */ +export type CancelAuthorizationResponse = Message<"engram.mcp.v1.CancelAuthorizationResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string flow_id = 2; + */ + flowId: string; + + /** + * @generated from field: string connection_id = 3; + */ + connectionId: string; + + /** + * @generated from field: string registration_id = 4; + */ + registrationId: string; +}; + +/** + * Describes the message engram.mcp.v1.CancelAuthorizationResponse. + * Use `create(CancelAuthorizationResponseSchema)` to create a new message. + */ +export const CancelAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 18); + /** * @generated from enum engram.mcp.v1.CredentialKind */ @@ -728,6 +799,14 @@ export const CredentialControl: GenService<{ input: typeof BeginAuthorizationRequestSchema; output: typeof BeginAuthorizationResponseSchema; }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.CancelAuthorization + */ + cancelAuthorization: { + methodKind: "unary"; + input: typeof CancelAuthorizationRequestSchema; + output: typeof CancelAuthorizationResponseSchema; + }, /** * @generated from rpc engram.mcp.v1.CredentialControl.CompleteAuthorization */ diff --git a/orchestrator/src/mcp/credential-admin.ts b/orchestrator/src/mcp/credential-admin.ts index 01ddb8bd7..3823257ac 100644 --- a/orchestrator/src/mcp/credential-admin.ts +++ b/orchestrator/src/mcp/credential-admin.ts @@ -5,6 +5,7 @@ import { CredentialKind, CredentialStatusSchema, type CredentialStatus, + type AuthorityContext, } from "../gen/engram/mcp/v1/control_pb.ts"; import type { CredentialControlClient } from "./control-client.ts"; import { @@ -144,7 +145,12 @@ export function makeCredentialAdmin(deps: { await store.stage(claim, { ...claim.metadata, [field]: statusMetadata(receipt) }, now()); return receipt; } - async function revokeTargetOAuth(claim: CredentialClaim, actorId: string, recover = false) { + async function revokeTargetOAuth( + claim: CredentialClaim, + actorId: string, + recover = false, + cancelledContext?: AuthorityContext, + ) { if (claim.config.auth.kind !== "oauth") return undefined; const registrationId = claim.config.auth.clientRegistrationId; const saved = decode(claim.metadata.status); @@ -178,11 +184,43 @@ export function makeCredentialAdmin(deps: { current.reference.registrationId !== registrationId ) throw new Error("stale OAuth authority"); - // Recovery owns the connection transition. Revoke the exact current snapshot, - // including a late callback result, without granting or restoring its authority. + if ( + recover && + !( + (saved?.context && + saved.reference?.kind === CredentialKind.OAUTH && + saved.reference.registrationId === registrationId && + equals(AuthorityContextSchema, current.context!, saved.context)) || + (cancelledContext && equals(AuthorityContextSchema, current.context!, cancelledContext)) + ) + ) + throw new Error("stale OAuth recovery authority"); + // Accept only the saved authority or this canceled attempt's exact context. + // A newer same-registration authority is not evidence for old recovery. await store.stage(claim, { ...claim.metadata, status: statusMetadata(current) }, now()); return revoke(claim, "status", actorId); } + async function cancelPending(claim: CredentialClaim, actorId: string) { + const flow = claim.metadata.flow; + if (!flow) return; + const receipt = await control.cancelAuthorization({ + protocol: 1, + flowId: flow.id, + connectionId: claim.id, + registrationId: flow.registrationId, + actorId, + }); + if ( + receipt.protocol !== 1 || + receipt.flowId !== flow.id || + receipt.connectionId !== claim.id || + receipt.registrationId !== flow.registrationId + ) + throw new Error("stale cancellation receipt"); + // Keep the attempt proof durable until the whole transition finishes. + // A later recovery may need it to identify a callback that won publication. + return flow; + } async function claimCurrent( id: string, revision: number, @@ -191,7 +229,10 @@ export function makeCredentialAdmin(deps: { config?: McpConfig, ) { const row = await store.get(id); - return store.claim(id, revision, generation, actor, config ?? row.config, now()); + const claim = await store.claim(id, revision, generation, actor, config ?? row.config, now()); + await cancelPending(claim, actor); + delete claim.metadata.flow; + return claim; } return { registrations: () => structuredClone(registrations), @@ -413,14 +454,18 @@ export function makeCredentialAdmin(deps: { }, async recover(id: string, revision: number, generation: number, actorId: string) { const claim = await store.recover(id, revision, generation, actorId, now()); + const cancelled = await cancelPending(claim, actorId); + const cancelledContext = cancelled + ? fromJson(AuthorityContextSchema, cancelled.context) + : undefined; if ( claim.config.auth.kind === "oauth" && decode(claim.metadata.status)?.reference?.kind === CredentialKind.OAUTH ) - await revokeTargetOAuth(claim, actorId, true); + await revokeTargetOAuth(claim, actorId, true, cancelledContext); else { await revoke(claim, "status", actorId); - await revokeTargetOAuth(claim, actorId, true); + await revokeTargetOAuth(claim, actorId, true, cancelledContext); } // Client-secret writes also need a durable fence after interrupted publication. await revoke(claim, "clientSecret", actorId); diff --git a/orchestrator/src/mcp/credential-store.ts b/orchestrator/src/mcp/credential-store.ts index b86d74f15..fde66a6db 100644 --- a/orchestrator/src/mcp/credential-store.ts +++ b/orchestrator/src/mcp/credential-store.ts @@ -110,7 +110,8 @@ export function makeCredentialMetadataStore(db = getDb()) { throw new Error("stale authorization flow"); const metadata = checkedMetadata(row.mcpCredential); metadata.transition = { id: crypto.randomUUID(), actorId, startedAt: now.getTime() }; - if (!callbackId) delete metadata.flow; + // Keep the exact pending attempt until the lifecycle adapter has durably + // canceled it. Clearing it here would lose the coordinator fence subject. const nextRevision = revision + (callbackId ? 0 : 1), nextGeneration = generation + (callbackId ? 0 : 1); await tx diff --git a/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts b/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts index 1e83c35c7..1e8b72210 100644 --- a/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts +++ b/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts @@ -14,6 +14,11 @@ export const putSecret = CredentialControl.method.putSecret; */ export const beginAuthorization = CredentialControl.method.beginAuthorization; +/** + * @generated from rpc engram.mcp.v1.CredentialControl.CancelAuthorization + */ +export const cancelAuthorization = CredentialControl.method.cancelAuthorization; + /** * @generated from rpc engram.mcp.v1.CredentialControl.CompleteAuthorization */ diff --git a/web/src/gen/engram/mcp/v1/control_pb.ts b/web/src/gen/engram/mcp/v1/control_pb.ts index 00333e847..c7c78e197 100644 --- a/web/src/gen/engram/mcp/v1/control_pb.ts +++ b/web/src/gen/engram/mcp/v1/control_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/control.proto. */ export const file_engram_mcp_v1_control: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDKowBCg5DcmVkZW50aWFsS2luZBIfChtDUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIbChdDUkVERU5USUFMX0tJTkRfQVBJX0tFWRABEiEKHUNSRURFTlRJQUxfS0lORF9DTElFTlRfU0VDUkVUEAISGQoVQ1JFREVOVElBTF9LSU5EX09BVVRIEAMykAQKEUNyZWRlbnRpYWxDb250cm9sEk0KCVB1dFNlY3JldBIfLmVuZ3JhbS5tY3AudjEuUHV0U2VjcmV0UmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmUKFUNvbXBsZXRlQXV0aG9yaXphdGlvbhIrLmVuZ3JhbS5tY3AudjEuQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJNCglHZXRTdGF0dXMSHy5lbmdyYW0ubWNwLnYxLkdldFN0YXR1c1JlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSRwoGUmV2b2tlEhwuZW5ncmFtLm1jcC52MS5SZXZva2VSZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEkIKBUxlYXNlEhsuZW5ncmFtLm1jcC52MS5MZWFzZVJlcXVlc3QaHC5lbmdyYW0ubWNwLnYxLkxlYXNlUmVzcG9uc2ViBnByb3RvMw"); + fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDIoEBChpDYW5jZWxBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJEhAKCGFjdG9yX2lkGAUgASgJInAKG0NhbmNlbEF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJKowBCg5DcmVkZW50aWFsS2luZBIfChtDUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIbChdDUkVERU5USUFMX0tJTkRfQVBJX0tFWRABEiEKHUNSRURFTlRJQUxfS0lORF9DTElFTlRfU0VDUkVUEAISGQoVQ1JFREVOVElBTF9LSU5EX09BVVRIEAMy/gQKEUNyZWRlbnRpYWxDb250cm9sEk0KCVB1dFNlY3JldBIfLmVuZ3JhbS5tY3AudjEuUHV0U2VjcmV0UmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmwKE0NhbmNlbEF1dGhvcml6YXRpb24SKS5lbmdyYW0ubWNwLnYxLkNhbmNlbEF1dGhvcml6YXRpb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DYW5jZWxBdXRob3JpemF0aW9uUmVzcG9uc2USZQoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEk0KCUdldFN0YXR1cxIfLmVuZ3JhbS5tY3AudjEuR2V0U3RhdHVzUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJHCgZSZXZva2USHC5lbmdyYW0ubWNwLnYxLlJldm9rZVJlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); /** * @generated from message engram.mcp.v1.Destination @@ -675,6 +675,77 @@ export type LeaseResponse = Message<"engram.mcp.v1.LeaseResponse"> & { export const LeaseResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_control, 16); +/** + * Exact attempt only. Cancellation persists in the existing flow row. + * + * @generated from message engram.mcp.v1.CancelAuthorizationRequest + */ +export type CancelAuthorizationRequest = Message<"engram.mcp.v1.CancelAuthorizationRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string flow_id = 2; + */ + flowId: string; + + /** + * @generated from field: string connection_id = 3; + */ + connectionId: string; + + /** + * @generated from field: string registration_id = 4; + */ + registrationId: string; + + /** + * @generated from field: string actor_id = 5; + */ + actorId: string; +}; + +/** + * Describes the message engram.mcp.v1.CancelAuthorizationRequest. + * Use `create(CancelAuthorizationRequestSchema)` to create a new message. + */ +export const CancelAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 17); + +/** + * @generated from message engram.mcp.v1.CancelAuthorizationResponse + */ +export type CancelAuthorizationResponse = Message<"engram.mcp.v1.CancelAuthorizationResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string flow_id = 2; + */ + flowId: string; + + /** + * @generated from field: string connection_id = 3; + */ + connectionId: string; + + /** + * @generated from field: string registration_id = 4; + */ + registrationId: string; +}; + +/** + * Describes the message engram.mcp.v1.CancelAuthorizationResponse. + * Use `create(CancelAuthorizationResponseSchema)` to create a new message. + */ +export const CancelAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 18); + /** * @generated from enum engram.mcp.v1.CredentialKind */ @@ -728,6 +799,14 @@ export const CredentialControl: GenService<{ input: typeof BeginAuthorizationRequestSchema; output: typeof BeginAuthorizationResponseSchema; }, + /** + * @generated from rpc engram.mcp.v1.CredentialControl.CancelAuthorization + */ + cancelAuthorization: { + methodKind: "unary"; + input: typeof CancelAuthorizationRequestSchema; + output: typeof CancelAuthorizationResponseSchema; + }, /** * @generated from rpc engram.mcp.v1.CredentialControl.CompleteAuthorization */ From 087c6a43376839a5b863c26a775e6c93f3337483 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 01:39:16 -0500 Subject: [PATCH 017/279] Add trusted MCP session write policy and fenced admission --- crates/engram-coordinator/src/api/sessions.rs | 22 +- .../src/grpc_app/convert.rs | 1 + crates/engram-coordinator/src/grpc_app/mod.rs | 2 +- .../src/grpc_app/session.rs | 15 + crates/engram-coordinator/src/lib.rs | 3 +- crates/engram-coordinator/src/mcp/control.rs | 101 +-- crates/engram-coordinator/src/mcp/mod.rs | 1 + crates/engram-coordinator/src/mcp/policy.rs | 284 ++++++++ crates/engram-coordinator/src/mcp/startup.rs | 10 +- crates/engram-coordinator/tests/grpc_app.rs | 31 + .../engram-coordinator/tests/mcp_control.rs | 161 ++++- .../tests/placement_reservation_live_pg.rs | 2 + .../tests/queue_scanner_live_pg.rs | 2 + crates/engram-core/src/traits/metadata.rs | 30 + crates/engram-core/src/types/mcp_policy.rs | 343 ++++++++++ crates/engram-core/src/types/mod.rs | 2 + crates/engram-dst-cosim/src/scheduler.rs | 1 + crates/engram-dst/src/scheduler.rs | 1 + crates/engram-dst/tests/exactly_once.rs | 1 + crates/engram-dst/tests/ttft_attach.rs | 1 + crates/engram-postgres/src/lib.rs | 94 +++ crates/engram-postgres/src/mcp_policy.rs | 174 +++++ crates/engram-protocol/build.rs | 13 +- .../proto/engram/app/v1/session.proto | 39 ++ .../proto/engram/mcp/v1/control.proto | 26 +- .../proto/engram/mcp/v1/session_policy.proto | 53 ++ crates/engram-protocol/src/mcp.rs | 3 +- .../engram-protocol/tests/mcp_policy_wire.rs | 33 + crates/engram-sim/src/meta/mcp_policy.rs | 142 ++++ crates/engram-sim/src/meta/mod.rs | 3 + crates/engram-sim/src/meta/store_impl.rs | 166 +++-- crates/engram-sim/tests/meta_conformance.rs | 606 ++++++++++++++++++ crates/engram-tui/src/net.rs | 1 + .../0128_mcp_session_write_policy.sql | 9 + docs/adr/0125-isolated-external-mcp.md | 47 +- mcp-worker/README.md | 6 + .../src/__tests__/mcp-control.test.ts | 70 +- .../__tests__/mcp-credential-admin.test.ts | 108 ++-- .../__tests__/mcp-credential-broker.test.ts | 125 ++-- .../__tests__/mcp-credential-oauth.test.ts | 145 +++-- .../mcp-session-policy-store.test.ts | 47 ++ .../src/__tests__/mcp-session-policy.test.ts | 194 ++++++ orchestrator/src/authz/policy-map.ts | 5 + .../src/gen/engram/app/v1/session_pb.ts | 319 +++++++-- .../src/gen/engram/mcp/v1/control_pb.ts | 96 ++- .../gen/engram/mcp/v1/session_policy_pb.ts | 273 ++++++++ orchestrator/src/index.ts | 3 +- orchestrator/src/mcp/control-client.ts | 106 +-- orchestrator/src/mcp/session-policy.ts | 173 +++++ orchestrator/src/rpc/passthrough.ts | 5 + .../v1/session-SessionService_connectquery.ts | 5 + web/src/gen/engram/app/v1/session_pb.ts | 319 +++++++-- ...l-CredentialControlService_connectquery.ts | 40 ++ .../control-CredentialControl_connectquery.ts | 40 -- web/src/gen/engram/mcp/v1/control_pb.ts | 96 ++- ...olicy-SessionPolicyService_connectquery.ts | 25 + .../gen/engram/mcp/v1/session_policy_pb.ts | 273 ++++++++ 57 files changed, 4388 insertions(+), 508 deletions(-) create mode 100644 crates/engram-coordinator/src/mcp/policy.rs create mode 100644 crates/engram-core/src/types/mcp_policy.rs create mode 100644 crates/engram-postgres/src/mcp_policy.rs create mode 100644 crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto create mode 100644 crates/engram-protocol/tests/mcp_policy_wire.rs create mode 100644 crates/engram-sim/src/meta/mcp_policy.rs create mode 100644 deploy/migrations/0128_mcp_session_write_policy.sql create mode 100644 orchestrator/src/__tests__/mcp-session-policy-store.test.ts create mode 100644 orchestrator/src/__tests__/mcp-session-policy.test.ts create mode 100644 orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts create mode 100644 orchestrator/src/mcp/session-policy.ts create mode 100644 web/src/gen/engram/mcp/v1/control-CredentialControlService_connectquery.ts delete mode 100644 web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts create mode 100644 web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts create mode 100644 web/src/gen/engram/mcp/v1/session_policy_pb.ts diff --git a/crates/engram-coordinator/src/api/sessions.rs b/crates/engram-coordinator/src/api/sessions.rs index 1193a312d..1bab92514 100644 --- a/crates/engram-coordinator/src/api/sessions.rs +++ b/crates/engram-coordinator/src/api/sessions.rs @@ -454,6 +454,9 @@ pub(crate) async fn load_session_secrets( #[derive(Deserialize)] pub struct CreateSessionRequest { + /// Supplied only by the dedicated pinned mTLS creation service. + #[serde(skip)] + pub(crate) mcp_creation: Option, /// ADR 0109: trusted app-gRPC callers can reserve an ID so their external /// authorization snapshot exists before the VM starts. The public JSON /// surface cannot set this field. @@ -596,7 +599,14 @@ pub(crate) async fn create_session_core( return result; } }; - let result = boot_prepared(state, prepared, start, req.oauth_credential.clone()).await; + let result = boot_prepared( + state, + prepared, + start, + req.oauth_credential.clone(), + req.mcp_creation.clone(), + ) + .await; let kind = match &result { Ok(body) => body.kind, Err(_) => "unknown", @@ -638,6 +648,7 @@ async fn boot_prepared( // (now-concurrent, host-side) restore work. create_start: std::time::Duration, oauth_credential: Option, + mcp_creation: Option, ) -> Result { let crate::session_boot::PreparedBoot { nbd_slot_need, @@ -745,7 +756,16 @@ async fn boot_prepared( None => None, }; + if mcp_creation.is_some() + && (!matches!(inputs.selected_harness.as_deref(), Some("claude" | "codex")) + || inputs.spec.mode != SessionMode::Agent) + { + return Err(ApiError::BadRequest( + "external write policy requires a supported agent harness".into(), + )); + } let write_set = engram_core::traits::SessionCreateWriteSet { + mcp_policy: mcp_creation, nbd_slot_need, session_id, spec: inputs.spec.clone(), diff --git a/crates/engram-coordinator/src/grpc_app/convert.rs b/crates/engram-coordinator/src/grpc_app/convert.rs index d2978d72b..265c49861 100644 --- a/crates/engram-coordinator/src/grpc_app/convert.rs +++ b/crates/engram-coordinator/src/grpc_app/convert.rs @@ -242,6 +242,7 @@ pub(crate) fn create_request_from_proto( }) .collect::, ApiError>>()?; Ok(CreateSessionRequest { + mcp_creation: None, requested_session_id, image: image_uri, mode, diff --git a/crates/engram-coordinator/src/grpc_app/mod.rs b/crates/engram-coordinator/src/grpc_app/mod.rs index a83682714..a1bd59d19 100644 --- a/crates/engram-coordinator/src/grpc_app/mod.rs +++ b/crates/engram-coordinator/src/grpc_app/mod.rs @@ -11,7 +11,7 @@ //! authz lives over there, not here. pub mod auth; -mod convert; +pub(crate) mod convert; mod fleet; mod harness_catalog; mod image; diff --git a/crates/engram-coordinator/src/grpc_app/session.rs b/crates/engram-coordinator/src/grpc_app/session.rs index 2b86d148c..71222dae4 100644 --- a/crates/engram-coordinator/src/grpc_app/session.rs +++ b/crates/engram-coordinator/src/grpc_app/session.rs @@ -72,6 +72,16 @@ impl app::session_service_server::SessionService for AppSessionService { })) } + async fn get_mcp_write_policy( + &self, + req: Request, + ) -> Result, Status> { + self.auth.check(&req)?; + Err(Status::failed_precondition( + "trusted policy transport required", + )) + } + async fn get_session( &self, req: Request, @@ -160,6 +170,11 @@ impl app::session_service_server::SessionService for AppSessionService { ) -> Result, Status> { self.auth.check(&req)?; let r = req.into_inner(); + if r.expected_mcp_plan_review.is_some() { + return Err(Status::failed_precondition( + "trusted policy transport required", + )); + } let id = parse_session_id(&r.session_id)?; let note = crate::api::prompt::complete_tool_call_core( &self.state, diff --git a/crates/engram-coordinator/src/lib.rs b/crates/engram-coordinator/src/lib.rs index ec3e97046..670e333c2 100644 --- a/crates/engram-coordinator/src/lib.rs +++ b/crates/engram-coordinator/src/lib.rs @@ -378,10 +378,11 @@ pub async fn run_with_registry_and_local( } // The optional credential service has its own authenticated listener. - let mcp_control = mcp::startup::start( + let mcp_control = mcp::startup::start_with_policy( cfg.mcp_control_config.as_deref(), state.oauth.clone(), state.subscribe_shutdown(), + Some(state.clone()), ) .await .map_err(|_| { diff --git a/crates/engram-coordinator/src/mcp/control.rs b/crates/engram-coordinator/src/mcp/control.rs index a6999f024..3a8cf0c25 100644 --- a/crates/engram-coordinator/src/mcp/control.rs +++ b/crates/engram-coordinator/src/mcp/control.rs @@ -5,7 +5,9 @@ use engram_protocol::mcp as wire; use sha2::{Digest, Sha256}; use std::{collections::BTreeSet, sync::Arc, time::Duration}; use tonic::{Request, Response, Status}; -use wire::credential_control_server::{CredentialControl, CredentialControlServer}; +use wire::credential_control_service_server::{ + CredentialControlService, CredentialControlServiceServer, +}; type RpcResult = std::result::Result, Status>; pub struct ControlTls { @@ -15,14 +17,21 @@ pub struct ControlTls { pub orchestrator_fingerprints: Vec, } #[derive(Clone)] -struct Control { - authority: Arc, +pub(super) struct Control { + pub(super) authority: Arc, pins: Arc>, admission: Arc, } pub fn server( authority: Arc, tls: ControlTls, +) -> super::Result { + server_with_policy(authority, tls, None) +} +pub fn server_with_policy( + authority: Arc, + tls: ControlTls, + state: Option, ) -> super::Result { if tls.orchestrator_fingerprints.is_empty() || tls.orchestrator_fingerprints.len() > 8 @@ -58,13 +67,23 @@ pub fn server( .timeout(Duration::from_secs(30)) .max_concurrent_streams(16) .add_service( - CredentialControlServer::new(service) + CredentialControlServiceServer::new(service.clone()) .max_decoding_message_size(32768) .max_encoding_message_size(32768), + ) + .add_service( + wire::session_policy_service_server::SessionPolicyServiceServer::new( + super::policy::PolicyControl { + control: service, + state, + }, + ) + .max_decoding_message_size(4 * 1024 * 1024) + .max_encoding_message_size(4 * 1024 * 1024), )) } impl Control { - fn check( + pub(super) fn check( &self, req: &Request, protocol: u32, @@ -243,11 +262,11 @@ fn status(v: wire::CredentialStatus) -> std::result::Result, - ) -> RpcResult { + ) -> RpcResult { let _permit = self.check(&req, req.get_ref().protocol)?; let v = req.into_inner(); actor(&v.actor_id)?; @@ -257,18 +276,20 @@ impl CredentialControl for Control { _ => return Err(Status::invalid_argument("invalid secret kind")), }; let expected = v.expected.map(reference).transpose()?; - Ok(Response::new(status_wire( - self.authority - .put_secret( - &change(v.context)?, - role, - expected.as_ref(), - &v.header, - &v.value, - ) - .await - .map_err(error)?, - ))) + Ok(Response::new(wire::PutSecretResponse { + status: Some(status_wire( + self.authority + .put_secret( + &change(v.context)?, + role, + expected.as_ref(), + &v.header, + &v.value, + ) + .await + .map_err(error)?, + )), + })) } async fn begin_authorization( &self, @@ -301,7 +322,7 @@ impl CredentialControl for Control { async fn complete_authorization( &self, req: Request, - ) -> RpcResult { + ) -> RpcResult { let _permit = self.check(&req, req.get_ref().protocol)?; let v = req.into_inner(); actor(&v.actor_id)?; @@ -318,9 +339,11 @@ impl CredentialControl for Control { code: v.code, actor_id: v.actor_id, }; - Ok(Response::new(status_wire( - self.authority.callback(&callback).await.map_err(error)?, - ))) + Ok(Response::new(wire::CompleteAuthorizationResponse { + status: Some(status_wire( + self.authority.callback(&callback).await.map_err(error)?, + )), + })) } async fn cancel_authorization( &self, @@ -347,26 +370,30 @@ impl CredentialControl for Control { async fn get_status( &self, req: Request, - ) -> RpcResult { + ) -> RpcResult { let _permit = self.check(&req, req.get_ref().protocol)?; let v = req.into_inner(); - Ok(Response::new(status_wire( - self.authority - .status(&v.connection_id, &reference(required(v.reference)?)?) - .await - .map_err(error)?, - ))) + Ok(Response::new(wire::GetStatusResponse { + status: Some(status_wire( + self.authority + .status(&v.connection_id, &reference(required(v.reference)?)?) + .await + .map_err(error)?, + )), + })) } - async fn revoke(&self, req: Request) -> RpcResult { + async fn revoke(&self, req: Request) -> RpcResult { let _permit = self.check(&req, req.get_ref().protocol)?; let v = req.into_inner(); actor(&v.actor_id)?; - Ok(Response::new(status_wire( - self.authority - .revoke(&status(required(v.expected)?)?) - .await - .map_err(error)?, - ))) + Ok(Response::new(wire::RevokeResponse { + status: Some(status_wire( + self.authority + .revoke(&status(required(v.expected)?)?) + .await + .map_err(error)?, + )), + })) } async fn lease(&self, req: Request) -> RpcResult { let _permit = self.check(&req, req.get_ref().protocol)?; diff --git a/crates/engram-coordinator/src/mcp/mod.rs b/crates/engram-coordinator/src/mcp/mod.rs index 00cf01ae1..81e092ad3 100644 --- a/crates/engram-coordinator/src/mcp/mod.rs +++ b/crates/engram-coordinator/src/mcp/mod.rs @@ -23,6 +23,7 @@ mod io; mod lifecycle; pub mod model; pub mod oauth; +mod policy; pub mod startup; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use engram_core::types::org_secret::{McpSecretReservation, McpSecretRole}; diff --git a/crates/engram-coordinator/src/mcp/policy.rs b/crates/engram-coordinator/src/mcp/policy.rs new file mode 100644 index 000000000..eae920d04 --- /dev/null +++ b/crates/engram-coordinator/src/mcp/policy.rs @@ -0,0 +1,284 @@ +//! Trusted policy service. It is never served on application gRPC. +use super::control::Control; +use crate::state::SharedState; +use engram_core::{types::mcp_policy::*, SessionId}; +use engram_protocol::{app, mcp as wire}; +use tonic::{Request, Response, Status}; +use wire::session_policy_service_server::SessionPolicyService; +#[derive(Clone)] +pub(super) struct PolicyControl { + pub control: Control, + pub state: Option, +} +fn id(value: &str) -> Result { + value + .parse() + .map_err(|_| Status::invalid_argument("invalid session")) +} +fn err(_: engram_core::MetaError) -> Status { + Status::failed_precondition("session policy changed or unavailable") +} +fn required(value: Option) -> Result { + value.ok_or_else(|| Status::invalid_argument("required policy field missing")) +} +fn policy_ref(r: WritePolicyRef) -> app::McpWritePolicyRef { + app::McpWritePolicyRef { + session_id: r.session_id.to_string(), + policy_revision: r.policy_revision, + binding_epoch: r.binding_epoch, + } +} +fn from_ref(r: app::McpWritePolicyRef) -> Result { + if r.policy_revision < 0 || r.binding_epoch < 0 { + return Err(Status::invalid_argument("invalid policy reference")); + } + Ok(WritePolicyRef { + session_id: id(&r.session_id)?, + policy_revision: r.policy_revision, + binding_epoch: r.binding_epoch, + }) +} +fn policy(p: TrustedWritePolicy) -> app::McpTrustedWritePolicy { + app::McpTrustedWritePolicy { + r#ref: Some(policy_ref(p.reference)), + write_eligibility: p.write_eligibility.as_str().into(), + cause: p.cause, + accepted_input_id: p.accepted_input_id, + authenticated_actor_id: p.authenticated_actor_id, + prompt_id: p.prompt_id, + plan_tool_call_id: p.plan_tool_call_id, + plan_request_digest: p.plan_request_digest, + ancestor_policy_refs: p.ancestor_policy_refs.into_iter().map(policy_ref).collect(), + current: p.current, + initialized: p.initialized, + } +} +fn review(r: PlanReview) -> app::McpPlanReview { + app::McpPlanReview { + policy_ref: Some(policy_ref(r.policy_ref)), + tool_call_id: r.tool_call_id, + run_id: r.run_id, + event_idx: r.event_idx, + request_digest: r.request_digest, + args_json: r.args_json, + } +} +fn from_review(r: app::McpPlanReview) -> Result { + Ok(PlanReview { + policy_ref: from_ref(required(r.policy_ref)?)?, + tool_call_id: r.tool_call_id, + run_id: r.run_id, + event_idx: r.event_idx, + request_digest: r.request_digest, + args_json: r.args_json, + }) +} +fn eligibility(s: &str) -> Result { + match s { + "unknown" => Ok(WriteEligibility::Unknown), + "read_only" => Ok(WriteEligibility::ReadOnly), + "write_eligible" => Ok(WriteEligibility::WriteEligible), + _ => Err(Status::invalid_argument("invalid write eligibility")), + } +} +fn creation(request: &wire::CreatePolicySessionRequest) -> Result { + if request.actor_id.is_empty() + || request.actor_id.len() > 256 + || request.input_id.is_empty() + || request.input_id.len() > 256 + { + return Err(Status::invalid_argument("creation identity required")); + } + let create = request + .create + .as_ref() + .ok_or_else(|| Status::invalid_argument("creation required"))?; + let eligibility = match create.harness_mode.as_deref() { + None | Some("" | "default") => WriteEligibility::WriteEligible, + Some("plan") => WriteEligibility::ReadOnly, + _ => return Err(Status::invalid_argument("unsupported policy mode")), + }; + Ok(McpPolicyCreation { + actor_id: request.actor_id.clone(), + input_id: request.input_id.clone(), + parent_session_id: request.parent_session_id.as_deref().map(id).transpose()?, + eligibility, + }) +} +#[tonic::async_trait] +impl SessionPolicyService for PolicyControl { + async fn create_policy_session( + &self, + req: Request, + ) -> Result, Status> { + let _admission = self.control.check(&req, req.get_ref().protocol)?; + self.control + .authority + .enabled() + .map_err(|_| Status::unavailable("policy unavailable"))?; + let context = creation(req.get_ref())?; + let r = required(req.into_inner().create)?; + let identity_env = r.harness_env.clone().into_iter().collect(); + let mut api = crate::grpc_app::convert::create_request_from_proto(r) + .map_err(|_| Status::invalid_argument("invalid creation request"))?; + api.mcp_creation = Some(context); + let state = self + .state + .as_ref() + .ok_or_else(|| Status::unavailable("creation unavailable"))?; + let result = crate::api::sessions::create_session_core(state, identity_env, api, None) + .await + .map_err(|_| Status::failed_precondition("session creation unavailable"))?; + Ok(Response::new(wire::CreatePolicySessionResponse { + session: Some(app::CreateSessionResponse { + session_id: result.session_id.to_string(), + status: result.status.into(), + image_version: result.image_version, + kind: result.kind.into(), + }), + })) + } + async fn read_write_policy( + &self, + req: Request, + ) -> Result, Status> { + let _admission = self.control.check(&req, req.get_ref().protocol)?; + self.control + .authority + .enabled() + .map_err(|_| Status::unavailable("policy unavailable"))?; + let r = req.into_inner(); + let sid = id(&r.session_id)?; + let meta = &self.control.authority.manager.meta; + let p = meta.read_mcp_write_policy(sid).await.map_err(err)?; + let reason = if !p.initialized { + "External writes are blocked because this session has no trusted initial policy. Start a new supported session." + } else { + match p.write_eligibility { + WriteEligibility::Unknown=>"External writes are blocked because this session or an ancestor has unknown policy.", + WriteEligibility::ReadOnly=>"External writes are blocked by this session or an ancestor. The owner or an administrator must approve the applicable current plan.", + WriteEligibility::WriteEligible=>"External writes still require tool-specific authorization.", + } + }; + let reviewed = match r.plan_tool_call_id { + Some(call) => { + let reviewed = meta.read_mcp_plan_review(sid, &call).await.map_err(err)?; + if reviewed.policy_ref != p.reference { + return Err(Status::failed_precondition( + "session policy changed; reload the plan", + )); + } + Some(review(reviewed)) + } + None => None, + }; + // Review includes its own revision. A restriction between these reads cannot relax policy. + Ok(Response::new(wire::ReadWritePolicyResponse { + state: Some(app::GetMcpWritePolicyResponse { + policy: Some(policy(p)), + plan_review: reviewed, + reason: reason.into(), + allowed_actions: Vec::new(), + }), + })) + } + async fn accept_plan_exit( + &self, + req: Request, + ) -> Result, Status> { + let _admission = self.control.check(&req, req.get_ref().protocol)?; + self.control + .authority + .enabled() + .map_err(|_| Status::unavailable("policy unavailable"))?; + let r = req.into_inner(); + let input = PlanAcceptance { + review: from_review(required(r.expected)?)?, + actor_id: r.actor_id, + result_json: r.result_json, + }; + let sid = input.review.policy_ref.session_id; + let p = self + .control + .authority + .manager + .meta + .accept_mcp_plan_exit(&input) + .await + .map_err(err)?; + if let Some(state) = &self.state { + crate::outbox_delivery::enqueue_deliver_op(state, sid).await; + state.outbox_wake.notify_one(); + } + Ok(Response::new(wire::AcceptPlanExitResponse { + policy: Some(policy(p)), + })) + } + async fn validate_dispatch_fence( + &self, + req: Request, + ) -> Result, Status> { + let _admission = self.control.check(&req, req.get_ref().protocol)?; + self.control + .authority + .enabled() + .map_err(|_| Status::unavailable("policy unavailable"))?; + let r = req.into_inner(); + let ctx = RequestPolicyContext { + session_id: id(&r.session_id)?, + run_id: r.run_id, + tool_call_id: r.tool_call_id, + accepted_policy_ref: from_ref(required(r.accepted_policy_ref)?)?, + accepted_write_eligibility: eligibility(&r.accepted_write_eligibility)?, + ancestor_policy_refs: r + .ancestor_policy_refs + .into_iter() + .map(from_ref) + .collect::>()?, + }; + let answer = self + .control + .authority + .manager + .meta + .validate_mcp_dispatch_fence(&ctx) + .await + .map_err(err)?; + Ok(Response::new(wire::ValidateDispatchFenceResponse { + protocol: wire::PROTOCOL, + status: match answer { + DispatchFence::Current => "current", + DispatchFence::Stale => "stale", + DispatchFence::Unknown => "unknown", + } + .into(), + })) + } +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn creation_policy_comes_only_from_explicit_supported_creation_input() { + let mut r = wire::CreatePolicySessionRequest { + protocol: 1, + actor_id: "owner-a".into(), + input_id: "create-a".into(), + create: Some(app::CreateSessionRequest::default()), + ..Default::default() + }; + assert_eq!( + creation(&r).unwrap().eligibility, + WriteEligibility::WriteEligible + ); + r.create.as_mut().unwrap().harness_mode = Some("plan".into()); + assert_eq!( + creation(&r).unwrap().eligibility, + WriteEligibility::ReadOnly + ); + r.create.as_mut().unwrap().harness_mode = Some("custom".into()); + assert!(creation(&r).is_err()); + r.actor_id.clear(); + assert!(creation(&r).is_err()); + } +} diff --git a/crates/engram-coordinator/src/mcp/startup.rs b/crates/engram-coordinator/src/mcp/startup.rs index 06593174b..c75729af8 100644 --- a/crates/engram-coordinator/src/mcp/startup.rs +++ b/crates/engram-coordinator/src/mcp/startup.rs @@ -46,9 +46,17 @@ async fn bounded_file(path: &Path) -> super::Result> { Ok(bytes) } pub async fn start( + path: Option<&Path>, + manager: Arc, + shutdown: tokio::sync::watch::Receiver, +) -> super::Result>> { + start_with_policy(path, manager, shutdown, None).await +} +pub async fn start_with_policy( path: Option<&Path>, manager: Arc, mut shutdown: tokio::sync::watch::Receiver, + state: Option, ) -> super::Result>> { let Some(path) = path else { return Ok(None) }; let config: OperatorConfig = @@ -77,7 +85,7 @@ pub async fn start( client_ca: bounded_file(&config.orchestrator_ca_file).await?, orchestrator_fingerprints: config.orchestrator_fingerprints, }; - let router = control::server(authority, tls)?; + let router = control::server_with_policy(authority, tls, state)?; let listener = tokio::net::TcpListener::bind(config.listen_addr) .await .map_err(|_| McpError::Unavailable)?; diff --git a/crates/engram-coordinator/tests/grpc_app.rs b/crates/engram-coordinator/tests/grpc_app.rs index 55d5875a8..694c2b055 100644 --- a/crates/engram-coordinator/tests/grpc_app.rs +++ b/crates/engram-coordinator/tests/grpc_app.rs @@ -485,6 +485,7 @@ async fn complete_tool_call_appends_submitted_event_and_enqueues_outbox() { service .complete_tool_call(complete_tool_call_request(app::CompleteToolCallRequest { + expected_mcp_plan_review: None, session_id: session_id.to_string(), tool_call_id: "call_1".into(), result_json: r#" { "saved": true } "#.into(), @@ -508,6 +509,32 @@ async fn complete_tool_call_appends_submitted_event_and_enqueues_outbox() { assert_eq!(row.payload["result_json"], r#" { "saved": true } "#); } +#[tokio::test] +async fn complete_tool_call_app_transport_cannot_accept_policy_context() { + let (state, meta) = test_state(vec![TEST_TOKEN.into()]); + let session_id = meta + .create_session(SessionSpec { + image: "localhost:5001/demo:warm".into(), + mode: Default::default(), + }) + .await + .expect("seed session"); + let error = complete_tool_call_service(state) + .complete_tool_call(complete_tool_call_request(app::CompleteToolCallRequest { + expected_mcp_plan_review: Some(app::McpPlanReview::default()), + session_id: session_id.to_string(), + tool_call_id: "plan-call".into(), + result_json: r#"{"decision":"approve"}"#.into(), + })) + .await + .expect_err("application transport must reject policy authority"); + assert_eq!(error.code(), tonic::Code::FailedPrecondition); + assert!(outbox_rows(&meta).is_empty()); + assert!(!session_events(&meta, session_id) + .iter() + .any(|(kind, _)| kind == "tool_result_submitted")); +} + #[tokio::test] async fn complete_tool_call_rejects_empty_tool_call_id() { let (state, meta) = test_state(vec![TEST_TOKEN.into()]); @@ -522,6 +549,7 @@ async fn complete_tool_call_rejects_empty_tool_call_id() { let err = service .complete_tool_call(complete_tool_call_request(app::CompleteToolCallRequest { + expected_mcp_plan_review: None, session_id: session_id.to_string(), tool_call_id: String::new(), result_json: "{}".into(), @@ -555,6 +583,7 @@ async fn complete_tool_call_rejects_terminal_session() { let err = service .complete_tool_call(complete_tool_call_request(app::CompleteToolCallRequest { + expected_mcp_plan_review: None, session_id: session_id.to_string(), tool_call_id: "call_1".into(), result_json: "{}".into(), @@ -586,6 +615,7 @@ async fn complete_tool_call_is_idempotent_on_tool_call_id() { for _ in 0..2 { service .complete_tool_call(complete_tool_call_request(app::CompleteToolCallRequest { + expected_mcp_plan_review: None, session_id: session_id.to_string(), tool_call_id: "call_1".into(), result_json: r#"{"saved":true}"#.into(), @@ -625,6 +655,7 @@ async fn complete_tool_call_namespaces_outbox_identity_by_session() { for session_id in [first, second] { service .complete_tool_call(complete_tool_call_request(app::CompleteToolCallRequest { + expected_mcp_plan_review: None, session_id: session_id.to_string(), tool_call_id: "shared-call".into(), result_json: r#"{"saved":true}"#.into(), diff --git a/crates/engram-coordinator/tests/mcp_control.rs b/crates/engram-coordinator/tests/mcp_control.rs index a9c0b3f22..c58f6cd16 100644 --- a/crates/engram-coordinator/tests/mcp_control.rs +++ b/crates/engram-coordinator/tests/mcp_control.rs @@ -116,10 +116,12 @@ async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() .await; if let Ok(channel) = no_cert { assert!( - wire::credential_control_client::CredentialControlClient::new(channel) - .get_status(wire::GetStatusRequest::default()) - .await - .is_err() + wire::credential_control_service_client::CredentialControlServiceClient::new( + channel + ) + .get_status(wire::GetStatusRequest::default()) + .await + .is_err() ); } let channel = endpoint @@ -131,8 +133,147 @@ async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() .connect() .await .unwrap(); - let mut client = wire::credential_control_client::CredentialControlClient::new(channel); + let mut policy_client = + wire::session_policy_service_client::SessionPolicyServiceClient::new(channel.clone()); + let sid = meta + .create_session(engram_core::types::SessionSpec { + image: "test.invalid/policy:latest".into(), + mode: Default::default(), + }) + .await + .unwrap(); + let policy = policy_client + .read_write_policy(wire::ReadWritePolicyRequest { + protocol: 1, + session_id: sid.to_string(), + plan_tool_call_id: None, + }) + .await; + if pinned { + assert_eq!( + policy + .unwrap() + .into_inner() + .state + .unwrap() + .policy + .unwrap() + .write_eligibility, + "unknown" + ); + assert_eq!( + policy_client + .read_write_policy(wire::ReadWritePolicyRequest { + protocol: 0, + session_id: sid.to_string(), + plan_tool_call_id: None + }) + .await + .unwrap_err() + .code(), + tonic::Code::InvalidArgument + ); + } else { + assert_eq!(policy.unwrap_err().code(), tonic::Code::PermissionDenied); + } + let mut client = + wire::credential_control_service_client::CredentialControlServiceClient::new(channel); if pinned { + use engram_core::types::mcp_policy::{McpPolicyCreation, WriteEligibility}; + let policy_sid = engram_core::SessionId(uuid::Uuid::from_u128(1200)); + meta.reserve_and_persist_create( + engram_core::traits::SessionCreateWriteSet { + mcp_policy: Some(McpPolicyCreation { + actor_id: "owner-a".into(), + input_id: "create-a".into(), + parent_session_id: None, + eligibility: WriteEligibility::ReadOnly, + }), + nbd_slot_need: 1, + session_id: policy_sid, + spec: engram_core::types::SessionSpec { + image: "test.invalid/policy:latest".into(), + mode: Default::default(), + }, + mem_budget_mib: 512, + cpu_budget_vcpus: 1, + sealed_secrets: None, + capabilities: Vec::new(), + integration_policy_json: None, + runtime_spec: engram_core::types::runtime_spec::RuntimeSpec::new( + Vec::new(), + Some("claude".into()), + None, + Vec::new(), + ), + oauth_binding: None, + }, + &[], + 0, + ) + .await + .unwrap(); + meta.append_session_event(policy_sid,"tool_call_requested",serde_json::json!({"name":"exit_plan_mode","tool_call_id":"plan-a","run_id":"run-a","args_json":"{}"})).await.unwrap(); + let reviewed = policy_client + .read_write_policy(wire::ReadWritePolicyRequest { + protocol: 1, + session_id: policy_sid.to_string(), + plan_tool_call_id: Some("plan-a".into()), + }) + .await + .unwrap() + .into_inner() + .state + .unwrap() + .plan_review + .unwrap(); + let approved = policy_client + .accept_plan_exit(wire::AcceptPlanExitRequest { + protocol: 1, + expected: Some(reviewed.clone()), + actor_id: "owner-a".into(), + result_json: "{\"decision\":\"approve\"}".into(), + }) + .await + .unwrap() + .into_inner() + .policy + .unwrap(); + assert_eq!(approved.write_eligibility, "write_eligible"); + assert_eq!(approved.authenticated_actor_id.as_deref(), Some("owner-a")); + let outbox = meta + .outbox_get(&engram_core::types::outbox::tool_result_outbox_id( + policy_sid, "plan-a", + )) + .await + .unwrap() + .unwrap(); + assert_eq!(outbox.payload["result_json"], "{\"decision\":\"approve\"}"); + let prior = policy_client + .validate_dispatch_fence(wire::ValidateDispatchFenceRequest { + protocol: 1, + session_id: policy_sid.to_string(), + run_id: "run-a".into(), + tool_call_id: "call-a".into(), + accepted_policy_ref: reviewed.policy_ref, + accepted_write_eligibility: "read_only".into(), + ancestor_policy_refs: Vec::new(), + }) + .await + .unwrap() + .into_inner(); + assert_ne!(prior.status, "current"); + assert_eq!( + policy_client + .create_policy_session(wire::CreatePolicySessionRequest { + protocol: 1, + ..Default::default() + }) + .await + .unwrap_err() + .code(), + tonic::Code::InvalidArgument + ); let missing = client .get_status(wire::GetStatusRequest { protocol: 1, @@ -222,10 +363,12 @@ async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() .await; if let Ok(channel) = plaintext { assert!( - wire::credential_control_client::CredentialControlClient::new(channel) - .get_status(wire::GetStatusRequest::default()) - .await - .is_err() + wire::credential_control_service_client::CredentialControlServiceClient::new( + channel + ) + .get_status(wire::GetStatusRequest::default()) + .await + .is_err() ); } let _ = stop.send(()); diff --git a/crates/engram-coordinator/tests/placement_reservation_live_pg.rs b/crates/engram-coordinator/tests/placement_reservation_live_pg.rs index ae0ed5506..a7365966c 100644 --- a/crates/engram-coordinator/tests/placement_reservation_live_pg.rs +++ b/crates/engram-coordinator/tests/placement_reservation_live_pg.rs @@ -123,6 +123,7 @@ fn bare_write_set( cpu_budget_vcpus: i32, ) -> SessionCreateWriteSet { SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id, spec: spec(), @@ -267,6 +268,7 @@ async fn reserve_and_persist_create_commits_the_full_write_set_together() { let session_id = SessionId::new(); let cap = Capability::parse("github:read@owner/repo").expect("valid capability"); let ws = SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id, spec: spec(), diff --git a/crates/engram-coordinator/tests/queue_scanner_live_pg.rs b/crates/engram-coordinator/tests/queue_scanner_live_pg.rs index 3ced7d572..75b86bde0 100644 --- a/crates/engram-coordinator/tests/queue_scanner_live_pg.rs +++ b/crates/engram-coordinator/tests/queue_scanner_live_pg.rs @@ -69,6 +69,7 @@ async fn enqueue( cpu_budget_vcpus: i32, ) { let ws = engram_core::traits::SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id, spec, @@ -111,6 +112,7 @@ async fn reserve( affinity_len: usize, ) -> Option { let ws = engram_core::traits::SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id, spec: spec.clone(), diff --git a/crates/engram-core/src/traits/metadata.rs b/crates/engram-core/src/traits/metadata.rs index a021463d3..aa1794da6 100644 --- a/crates/engram-core/src/traits/metadata.rs +++ b/crates/engram-core/src/traits/metadata.rs @@ -51,6 +51,7 @@ pub enum DisableEnabledImageOutcome { /// to commit in a single transaction, before any host RPC. #[derive(Clone, Debug)] pub struct SessionCreateWriteSet { + pub mcp_policy: Option, /// One root slot plus one slot when swap is enabled. pub nbd_slot_need: u32, pub session_id: SessionId, @@ -241,6 +242,35 @@ pub trait MetadataStore: Send + Sync { } // ---- sessions ---- + /// Read durable server policy and all current ancestors in one locked snapshot. + async fn read_mcp_write_policy( + &self, + _id: SessionId, + ) -> Result { + Err(MetaError::Conflict("policy unavailable".into())) + } + async fn read_mcp_plan_review( + &self, + _id: SessionId, + _call: &str, + ) -> Result { + Err(MetaError::Conflict("policy unavailable".into())) + } + /// Commit human decision, policy and original result delivery together. + async fn accept_mcp_plan_exit( + &self, + _input: &crate::types::mcp_policy::PlanAcceptance, + ) -> Result { + Err(MetaError::Conflict("policy unavailable".into())) + } + /// Call only after the external-write journal barrier. No grant is reusable. + async fn validate_mcp_dispatch_fence( + &self, + _context: &crate::types::mcp_policy::RequestPolicyContext, + ) -> Result { + Err(MetaError::Conflict("policy unavailable".into())) + } + async fn create_session(&self, spec: SessionSpec) -> Result; /// Atomically insert a session in `Created` status with `host_id` diff --git a/crates/engram-core/src/types/mcp_policy.rs b/crates/engram-core/src/types/mcp_policy.rs new file mode 100644 index 000000000..132fd18bb --- /dev/null +++ b/crates/engram-core/src/types/mcp_policy.rs @@ -0,0 +1,343 @@ +//! Conservative server authorization, independent of guest mode reports. +use super::event::PersistedEvent; +use crate::{MetaError, SessionId}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum WriteEligibility { + Unknown, + ReadOnly, + WriteEligible, +} +impl WriteEligibility { + pub fn as_str(self) -> &'static str { + match self { + Self::Unknown => "unknown", + Self::ReadOnly => "read_only", + Self::WriteEligible => "write_eligible", + } + } +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WritePolicyRef { + pub session_id: SessionId, + pub policy_revision: i64, + pub binding_epoch: i64, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpPolicyCreation { + pub actor_id: String, + pub input_id: String, + pub parent_session_id: Option, + pub eligibility: WriteEligibility, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoredWritePolicy { + pub revision: i64, + pub eligibility: WriteEligibility, + pub initialized: bool, + pub cause: String, + pub accepted_input_id: String, + pub actor_id: Option, + pub prompt_id: Option, + pub plan_tool_call_id: Option, + pub plan_request_digest: Option, + /// Requests before the accepted restriction cannot approve that restriction. + pub event_floor: i64, +} +impl StoredWritePolicy { + pub fn initial(input: &McpPolicyCreation) -> Self { + Self { + revision: 1, + eligibility: input.eligibility, + initialized: input.eligibility != WriteEligibility::Unknown, + cause: "initial".into(), + accepted_input_id: input.input_id.clone(), + actor_id: Some(input.actor_id.clone()), + prompt_id: None, + plan_tool_call_id: None, + plan_request_digest: None, + event_floor: 0, + } + } + pub fn restrict( + previous: Option<&Self>, + input_id: &str, + event_floor: i64, + ) -> Result { + Ok(Self { + revision: previous + .map_or(0, |p| p.revision) + .checked_add(1) + .ok_or_else(|| conflict("policy revision exhausted"))?, + eligibility: WriteEligibility::ReadOnly, + initialized: previous.is_some_and(|p| p.initialized), + cause: "plan_accept".into(), + accepted_input_id: input_id.into(), + actor_id: None, + prompt_id: Some(input_id.into()), + plan_tool_call_id: None, + plan_request_digest: None, + event_floor, + }) + } +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TrustedWritePolicy { + #[serde(rename = "ref")] + pub reference: WritePolicyRef, + pub write_eligibility: WriteEligibility, + pub cause: String, + pub accepted_input_id: String, + pub authenticated_actor_id: Option, + pub prompt_id: Option, + pub plan_tool_call_id: Option, + pub plan_request_digest: Option, + pub ancestor_policy_refs: Vec, + pub current: bool, + pub initialized: bool, +} +/// Internal row view. The parent comes from persistence, never caller-supplied ancestry. +#[derive(Clone, Debug)] +pub struct PolicyNode { + pub id: SessionId, + pub binding_epoch: i64, + pub parent: Option, + pub policy: Option, + pub terminal: bool, + pub active: bool, +} +impl PolicyNode { + pub fn reference(&self) -> WritePolicyRef { + WritePolicyRef { + session_id: self.id, + policy_revision: self.policy.as_ref().map_or(0, |p| p.revision), + binding_epoch: self.binding_epoch, + } + } +} +pub fn compose_policy(nodes: &[PolicyNode]) -> Result { + let leaf = nodes.first().ok_or(MetaError::NotFound)?; + let mut eligibility = WriteEligibility::WriteEligible; + let mut current = true; + for node in nodes { + let e = node + .policy + .as_ref() + .map_or(WriteEligibility::Unknown, |p| p.eligibility); + current &= !node.terminal; + eligibility = match (eligibility, e) { + (WriteEligibility::Unknown, _) | (_, WriteEligibility::Unknown) => { + WriteEligibility::Unknown + } + (WriteEligibility::ReadOnly, _) | (_, WriteEligibility::ReadOnly) => { + WriteEligibility::ReadOnly + } + _ => WriteEligibility::WriteEligible, + }; + } + if !current { + eligibility = WriteEligibility::Unknown; + } + let p = leaf.policy.as_ref(); + Ok(TrustedWritePolicy { + reference: leaf.reference(), + write_eligibility: eligibility, + cause: p.map_or("unknown", |p| p.cause.as_str()).into(), + accepted_input_id: p.map_or("", |p| p.accepted_input_id.as_str()).into(), + authenticated_actor_id: p.and_then(|p| p.actor_id.clone()), + prompt_id: p.and_then(|p| p.prompt_id.clone()), + plan_tool_call_id: p.and_then(|p| p.plan_tool_call_id.clone()), + plan_request_digest: p.and_then(|p| p.plan_request_digest.clone()), + ancestor_policy_refs: nodes.iter().skip(1).map(PolicyNode::reference).collect(), + current, + initialized: p.is_some_and(|p| p.initialized), + }) +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlanReview { + pub policy_ref: WritePolicyRef, + pub tool_call_id: String, + pub run_id: String, + pub event_idx: i64, + pub request_digest: String, + pub args_json: String, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlanAcceptance { + pub review: PlanReview, + pub actor_id: String, + pub result_json: String, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestPolicyContext { + pub session_id: SessionId, + pub run_id: String, + pub tool_call_id: String, + pub accepted_policy_ref: WritePolicyRef, + pub accepted_write_eligibility: WriteEligibility, + pub ancestor_policy_refs: Vec, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DispatchFence { + Current, + Stale, + Unknown, +} +pub fn conflict(message: &str) -> MetaError { + MetaError::Conflict(message.into()) +} +/// Canonical envelope preserves the exact submitted argument bytes. +fn request_digest(event: &PersistedEvent) -> String { + let p = &event.payload; + let bytes=serde_json::to_vec(&serde_json::json!({"run_id":p["run_id"],"tool_call_id":p["tool_call_id"],"name":p["name"],"args_json":p["args_json"]})).expect("JSON value serializes"); + hex::encode(Sha256::digest(bytes)) +} +pub fn pending_request<'a>( + events: &'a [PersistedEvent], + call: &str, +) -> Result<&'a PersistedEvent, MetaError> { + let relevant: Vec<_> = events + .iter() + .filter(|e| { + e.payload["tool_call_id"].as_str() == Some(call) + && matches!( + e.kind.as_str(), + "tool_call_requested" | "tool_result_submitted" | "tool_call_completed" + ) + }) + .collect(); + if relevant.len() >= 1024 { + return Err(conflict("request history limit exceeded")); + } + let first = relevant + .iter() + .find(|e| e.kind == "tool_call_requested") + .ok_or_else(|| conflict("pending request missing"))?; + if first.rewound_at.is_some() + || relevant.iter().any(|e| { + matches!( + e.kind.as_str(), + "tool_result_submitted" | "tool_call_completed" + ) || (e.kind == "tool_call_requested" && request_digest(e) != request_digest(first)) + }) + { + return Err(conflict("pending request changed")); + } + Ok(first) +} +pub fn plan_review( + node: &PolicyNode, + events: &[PersistedEvent], + call: &str, +) -> Result { + let policy = node + .policy + .as_ref() + .ok_or_else(|| conflict("policy unknown"))?; + if node.terminal || !policy.initialized || policy.eligibility != WriteEligibility::ReadOnly { + return Err(conflict("known restrictive policy required")); + } + let event = pending_request(events, call)?; + if event.idx < policy.event_floor || event.payload["name"].as_str() != Some("exit_plan_mode") { + return Err(conflict("current plan request required")); + } + let run_id = event.payload["run_id"] + .as_str() + .filter(|s| !s.is_empty()) + .ok_or_else(|| conflict("run required"))?; + let args_json = event.payload["args_json"] + .as_str() + .ok_or_else(|| conflict("plan arguments required"))?; + Ok(PlanReview { + policy_ref: node.reference(), + tool_call_id: call.into(), + run_id: run_id.into(), + event_idx: event.idx, + request_digest: request_digest(event), + args_json: args_json.into(), + }) +} +pub fn accept_plan( + node: &PolicyNode, + events: &[PersistedEvent], + input: &PlanAcceptance, +) -> Result { + if input.actor_id.is_empty() + || input.actor_id.len() > 256 + || plan_review(node, events, &input.review.tool_call_id)? != input.review + { + return Err(conflict("plan review changed")); + } + let result: serde_json::Value = + serde_json::from_str(&input.result_json).map_err(|_| conflict("invalid plan result"))?; + if result["decision"] != "approve" { + return Err(conflict("only human approval can relax policy")); + } + let mut p = node + .policy + .clone() + .ok_or_else(|| conflict("policy unknown"))?; + p.revision = p + .revision + .checked_add(1) + .ok_or_else(|| conflict("policy revision exhausted"))?; + p.eligibility = WriteEligibility::WriteEligible; + p.cause = "owner_admin_plan_exit".into(); + p.accepted_input_id = super::outbox::tool_result_outbox_id(node.id, &input.review.tool_call_id); + p.actor_id = Some(input.actor_id.clone()); + p.plan_tool_call_id = Some(input.review.tool_call_id.clone()); + p.plan_request_digest = Some(input.review.request_digest.clone()); + Ok(p) +} +pub fn dispatch_fence( + nodes: &[PolicyNode], + events: &[PersistedEvent], + ctx: &RequestPolicyContext, +) -> Result { + let p = compose_policy(nodes)?; + if !nodes[0].active { + return Ok(DispatchFence::Stale); + } + if ctx.accepted_write_eligibility != WriteEligibility::WriteEligible + || p.write_eligibility != WriteEligibility::WriteEligible + { + return Ok(DispatchFence::Unknown); + } + if ctx.session_id != ctx.accepted_policy_ref.session_id + || p.reference != ctx.accepted_policy_ref + || p.ancestor_policy_refs != ctx.ancestor_policy_refs + { + return Ok(DispatchFence::Stale); + } + let request = match pending_request(events, &ctx.tool_call_id) { + Ok(r) => r, + Err(_) => return Ok(DispatchFence::Stale), + }; + let last_run = events.iter().rev().find(|e| { + e.rewound_at.is_none() + && matches!( + e.kind.as_str(), + "run_started" | "run_completed" | "run_interrupted" + ) + }); + if ctx.run_id.is_empty() + || request.payload["run_id"].as_str() != Some(&ctx.run_id) + || last_run.is_none_or(|e| { + e.kind != "run_started" || e.payload["run_id"].as_str() != Some(&ctx.run_id) + }) + { + return Ok(DispatchFence::Stale); + } + Ok(DispatchFence::Current) +} diff --git a/crates/engram-core/src/types/mod.rs b/crates/engram-core/src/types/mod.rs index 1f7909c20..7f8ba0ac8 100644 --- a/crates/engram-core/src/types/mod.rs +++ b/crates/engram-core/src/types/mod.rs @@ -55,3 +55,5 @@ pub use snapshot::*; pub mod teleport; pub use teleport::*; + +pub mod mcp_policy; diff --git a/crates/engram-dst-cosim/src/scheduler.rs b/crates/engram-dst-cosim/src/scheduler.rs index 64d28b3d8..d0a510a1e 100644 --- a/crates/engram-dst-cosim/src/scheduler.rs +++ b/crates/engram-dst-cosim/src/scheduler.rs @@ -120,6 +120,7 @@ impl Cosim { use engram_core::traits::Entropy as _; let session_id = SessionId::from(self.world.entropy.uuid()); let ws = SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id, spec: SessionSpec { diff --git a/crates/engram-dst/src/scheduler.rs b/crates/engram-dst/src/scheduler.rs index 8ff81683e..ce4efcfa9 100644 --- a/crates/engram-dst/src/scheduler.rs +++ b/crates/engram-dst/src/scheduler.rs @@ -621,6 +621,7 @@ impl Sim { use engram_core::traits::Entropy as _; let session_id = SessionId::from(self.world.entropy.uuid()); let ws = SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id, spec: SessionSpec { diff --git a/crates/engram-dst/tests/exactly_once.rs b/crates/engram-dst/tests/exactly_once.rs index 75e33fc8b..a35c48591 100644 --- a/crates/engram-dst/tests/exactly_once.rs +++ b/crates/engram-dst/tests/exactly_once.rs @@ -44,6 +44,7 @@ async fn reserve_session(sim: &mut Sim) -> (engram_coordinator::state::SharedSta .expect("replica 0 is up"); let session_id = SessionId::from(sim.world.entropy.uuid()); let ws = SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id, spec: SessionSpec { diff --git a/crates/engram-dst/tests/ttft_attach.rs b/crates/engram-dst/tests/ttft_attach.rs index 21568f7bf..0d1a10232 100644 --- a/crates/engram-dst/tests/ttft_attach.rs +++ b/crates/engram-dst/tests/ttft_attach.rs @@ -61,6 +61,7 @@ async fn boot_session( .expect("replica 0 is up"); let session_id = SessionId::from(sim.world.entropy.uuid()); let ws = SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id, spec: SessionSpec { diff --git a/crates/engram-postgres/src/lib.rs b/crates/engram-postgres/src/lib.rs index 26c66d61c..f6c33c6ad 100644 --- a/crates/engram-postgres/src/lib.rs +++ b/crates/engram-postgres/src/lib.rs @@ -34,6 +34,7 @@ use sqlx::postgres::{PgPool, PgPoolOptions}; use sqlx::Row; use uuid::Uuid; +mod mcp_policy; mod row; #[derive(Clone)] @@ -1515,6 +1516,32 @@ impl MetadataStore for PostgresStore { .map_err(db_err) } + async fn read_mcp_write_policy( + &self, + id: SessionId, + ) -> Result { + self.policy_read(id).await + } + async fn read_mcp_plan_review( + &self, + id: SessionId, + call: &str, + ) -> Result { + self.policy_review(id, call).await + } + async fn accept_mcp_plan_exit( + &self, + input: &engram_core::types::mcp_policy::PlanAcceptance, + ) -> Result { + self.policy_accept(input).await + } + async fn validate_mcp_dispatch_fence( + &self, + ctx: &engram_core::types::mcp_policy::RequestPolicyContext, + ) -> Result { + self.policy_fence(ctx).await + } + async fn create_session(&self, spec: SessionSpec) -> Result { let id = self.entropy.uuid(); let now = self.clock.now_utc(); @@ -1675,6 +1702,32 @@ impl MetadataStore for PostgresStore { } }; + if let Some(policy) = &ws.mcp_policy { + if policy.actor_id.is_empty() + || policy.input_id.is_empty() + || policy.parent_session_id == Some(ws.session_id) + { + return Err(MetaError::Conflict("invalid policy creation".into())); + } + if let Some(parent) = policy.parent_session_id { + mcp_policy::nodes(&mut tx, parent).await?; + } + sqlx::query( + "UPDATE sessions SET mcp_write_policy=$2,mcp_policy_parent_id=$3 WHERE id=$1", + ) + .bind(ws.session_id.as_uuid()) + .bind( + serde_json::to_value(engram_core::types::mcp_policy::StoredWritePolicy::initial( + policy, + )) + .map_err(|_| MetaError::Conflict("policy encode failed".into()))?, + ) + .bind(policy.parent_session_id.map(|id| id.as_uuid())) + .execute(&mut *tx) + .await + .map_err(db_err)?; + } + // -------- satellites, in the SAME transaction (issue #535 (b)) -------- // `harness` already rode the row INSERT above (mirrored from the // RuntimeSpec); the remaining satellites keep their own tables (FK'd @@ -3940,6 +3993,12 @@ impl MetadataStore for PostgresStore { } let mut tx = self.pool.begin().await.map_err(db_err)?; + sqlx::query("SELECT id FROM sessions WHERE id=$1 FOR UPDATE") + .bind(session_id.as_uuid()) + .fetch_optional(&mut *tx) + .await + .map_err(db_err)? + .ok_or(MetaError::NotFound)?; let inserted = sqlx::query( r#" INSERT INTO session_outbox @@ -4041,6 +4100,12 @@ impl MetadataStore for PostgresStore { )); } let mut tx = self.pool.begin().await.map_err(db_err)?; + sqlx::query("SELECT id FROM sessions WHERE id=$1 FOR UPDATE") + .bind(session_id.as_uuid()) + .fetch_optional(&mut *tx) + .await + .map_err(db_err)? + .ok_or(MetaError::NotFound)?; let inserted = sqlx::query( r#" INSERT INTO session_outbox @@ -4098,6 +4163,35 @@ impl MetadataStore for PostgresStore { return Ok(None); } + if outbox.kind == engram_core::types::outbox::OutboxKind::Prompt + && outbox.payload["mode"] == "plan" + { + let row = + sqlx::query("SELECT mcp_write_policy,next_event_idx FROM sessions WHERE id=$1") + .bind(session_id.as_uuid()) + .fetch_one(&mut *tx) + .await + .map_err(db_err)?; + let old: Option = row.try_get("mcp_write_policy").map_err(db_err)?; + let old = old + .map(serde_json::from_value) + .transpose() + .map_err(|_| MetaError::Conflict("policy unavailable".into()))?; + let policy = engram_core::types::mcp_policy::StoredWritePolicy::restrict( + old.as_ref(), + &outbox.prompt_id, + row.try_get("next_event_idx").map_err(db_err)?, + )?; + sqlx::query("UPDATE sessions SET mcp_write_policy=$2 WHERE id=$1") + .bind(session_id.as_uuid()) + .bind( + serde_json::to_value(policy) + .map_err(|_| MetaError::Conflict("policy encode failed".into()))?, + ) + .execute(&mut *tx) + .await + .map_err(db_err)?; + } let mut idxs = Vec::with_capacity(events.len()); for (kind, payload) in events { let event = sqlx::query( diff --git a/crates/engram-postgres/src/mcp_policy.rs b/crates/engram-postgres/src/mcp_policy.rs new file mode 100644 index 000000000..c3e868d11 --- /dev/null +++ b/crates/engram-postgres/src/mcp_policy.rs @@ -0,0 +1,174 @@ +use super::{db_err, PostgresStore}; +use engram_core::{ + types::{ + event::PersistedEvent, + mcp_policy::*, + outbox::{tool_result_outbox_id, OutboxKind}, + }, + MetaError, SessionId, +}; +use sqlx::{Postgres, Row, Transaction}; +use std::collections::BTreeSet; + +pub(super) async fn nodes( + tx: &mut Transaction<'_, Postgres>, + id: SessionId, +) -> Result, MetaError> { + // Parent links never change. Read the chain, then lock every row in ID order. + let mut chain = Vec::new(); + let mut seen = BTreeSet::new(); + let mut next = Some(id); + while let Some(id) = next { + if chain.len() >= 32 || !seen.insert(id) { + return Err(conflict("policy ancestry unavailable")); + } + let row = sqlx::query("SELECT mcp_policy_parent_id FROM sessions WHERE id=$1") + .bind(id.as_uuid()) + .fetch_optional(&mut **tx) + .await + .map_err(db_err)? + .ok_or(MetaError::NotFound)?; + next = row + .try_get::, _>("mcp_policy_parent_id") + .map_err(db_err)? + .map(SessionId); + chain.push(id); + } + let ids: Vec<_> = seen.iter().map(|id| id.as_uuid()).collect(); + let rows=sqlx::query("SELECT id, binding_epoch, mcp_policy_parent_id, mcp_write_policy, status FROM sessions WHERE id=ANY($1) ORDER BY id FOR UPDATE") + .bind(ids).fetch_all(&mut **tx).await.map_err(db_err)?; + if rows.len() != chain.len() { + return Err(conflict("policy ancestry unavailable")); + } + let mut result = Vec::new(); + for id in chain { + let row = rows + .iter() + .find(|r| r.get::("id") == id.as_uuid()) + .ok_or(MetaError::NotFound)?; + let raw: Option = row.try_get("mcp_write_policy").map_err(db_err)?; + let policy = raw + .map(serde_json::from_value) + .transpose() + .map_err(|_| conflict("policy unavailable"))?; + let status: String = row.try_get("status").map_err(db_err)?; + result.push(PolicyNode { + id, + binding_epoch: row.try_get("binding_epoch").map_err(db_err)?, + parent: row + .try_get::, _>("mcp_policy_parent_id") + .map_err(db_err)? + .map(SessionId), + policy, + terminal: matches!(status.as_str(), "dead" | "completed" | "failed"), + active: status == "active", + }); + } + Ok(result) +} +async fn events( + tx: &mut Transaction<'_, Postgres>, + id: SessionId, + call: &str, +) -> Result, MetaError> { + // At most one run event accompanies call history. A truncated history always + // contains at least 1024 call events, which the shared validator rejects. + let rows=sqlx::query("SELECT idx,kind,payload,created_at,recovery_epoch::bigint AS recovery_epoch,rewound_at FROM session_events WHERE session_id=$1 AND ((payload->>'tool_call_id'=$2 AND kind IN ('tool_call_requested','tool_result_submitted','tool_call_completed')) OR idx=(SELECT max(idx) FROM session_events WHERE session_id=$1 AND rewound_at IS NULL AND kind IN ('run_started','run_completed','run_interrupted'))) ORDER BY idx LIMIT 1025") + .bind(id.as_uuid()).bind(call).fetch_all(&mut **tx).await.map_err(db_err)?; + rows.into_iter() + .map(|r| { + Ok(PersistedEvent { + idx: r.try_get("idx").map_err(db_err)?, + kind: r.try_get("kind").map_err(db_err)?, + payload: r.try_get("payload").map_err(db_err)?, + created_at: r.try_get("created_at").map_err(db_err)?, + recovery_epoch: r.try_get("recovery_epoch").map_err(db_err)?, + rewound_at: r.try_get("rewound_at").map_err(db_err)?, + }) + }) + .collect() +} +impl PostgresStore { + pub(super) async fn policy_read(&self, id: SessionId) -> Result { + let mut tx = self.pool.begin().await.map_err(db_err)?; + let p = compose_policy(&nodes(&mut tx, id).await?)?; + tx.commit().await.map_err(db_err)?; + Ok(p) + } + pub(super) async fn policy_review( + &self, + id: SessionId, + call: &str, + ) -> Result { + let mut tx = self.pool.begin().await.map_err(db_err)?; + let ns = nodes(&mut tx, id).await?; + let p = plan_review(&ns[0], &events(&mut tx, id, call).await?, call)?; + tx.commit().await.map_err(db_err)?; + Ok(p) + } + pub(super) async fn policy_fence( + &self, + ctx: &RequestPolicyContext, + ) -> Result { + let mut tx = self.pool.begin().await.map_err(db_err)?; + let ns = nodes(&mut tx, ctx.session_id).await?; + let answer = dispatch_fence( + &ns, + &events(&mut tx, ctx.session_id, &ctx.tool_call_id).await?, + ctx, + )?; + tx.commit().await.map_err(db_err)?; + Ok(answer) + } + pub(super) async fn policy_accept( + &self, + input: &PlanAcceptance, + ) -> Result { + let id = input.review.policy_ref.session_id; + let now = self.clock.now_utc(); + let mut tx = self.pool.begin().await.map_err(db_err)?; + let mut ns = nodes(&mut tx, id).await?; + let outbox_id = tool_result_outbox_id(id, &input.review.tool_call_id); + let payload = serde_json::json!({"tool_call_id":input.review.tool_call_id,"result_json":input.result_json,"mcp_policy_acceptance":input}); + if let Some(row) = sqlx::query( + "SELECT payload FROM session_outbox WHERE prompt_id=$1 AND session_id=$2 AND kind=$3", + ) + .bind(&outbox_id) + .bind(id.as_uuid()) + .bind(OutboxKind::ToolResult.as_str()) + .fetch_optional(&mut *tx) + .await + .map_err(db_err)? + { + if row + .try_get::("payload") + .map_err(db_err)? + != payload + { + return Err(conflict("plan result already accepted")); + } + let p = compose_policy(&ns)?; + tx.commit().await.map_err(db_err)?; + return Ok(p); + } + let updated = accept_plan( + &ns[0], + &events(&mut tx, id, &input.review.tool_call_id).await?, + input, + )?; + sqlx::query("INSERT INTO session_outbox(prompt_id,session_id,kind,payload,created_at,not_before) VALUES($1,$2,$3,$4,$5,$5)").bind(&outbox_id).bind(id.as_uuid()).bind(OutboxKind::ToolResult.as_str()).bind(payload).bind(now).execute(&mut *tx).await.map_err(db_err)?; + let idx:i64=sqlx::query_scalar("UPDATE sessions SET mcp_write_policy=$2,next_event_idx=next_event_idx+1,last_event_at=$3,updated_at=$3 WHERE id=$1 RETURNING next_event_idx-1").bind(id.as_uuid()).bind(serde_json::to_value(&updated).map_err(|_|conflict("policy encode failed"))?).bind(now).fetch_one(&mut *tx).await.map_err(db_err)?; + let event = serde_json::json!({"tool_call_id":input.review.tool_call_id,"result_json":input.result_json,"at":now}); + sqlx::query("INSERT INTO session_events(session_id,idx,kind,payload,recovery_epoch,created_at) SELECT id,$2,'tool_result_submitted',$3,recovery_epoch,$4 FROM sessions WHERE id=$1").bind(id.as_uuid()).bind(idx).bind(event).bind(now).execute(&mut *tx).await.map_err(db_err)?; + sqlx::query("SELECT pg_notify('session_events',$1),pg_notify('session_outbox',$2)") + .bind(serde_json::json!({"session_id":id,"idx":idx}).to_string()) + .bind(id.to_string()) + .execute(&mut *tx) + .await + .map_err(db_err)?; + ns[0].policy = Some(updated); + let p = compose_policy(&ns)?; + tx.commit().await.map_err(db_err)?; + Ok(p) + } +} diff --git a/crates/engram-protocol/build.rs b/crates/engram-protocol/build.rs index 11e8704d2..ae1e83740 100644 --- a/crates/engram-protocol/build.rs +++ b/crates/engram-protocol/build.rs @@ -47,15 +47,26 @@ fn main() -> Result<(), Box> { // reach 16 MiB at 1M IDs; the host server bumps its decode // cap inline at the call site rather than globally here. .compile_protos(&protos, &includes)?; + let mcp_out = std::path::PathBuf::from(std::env::var("OUT_DIR")?).join("mcp"); + std::fs::create_dir_all(&mcp_out)?; tonic_prost_build::configure() + .out_dir(mcp_out) .build_server(true) .build_client(true) + .extern_path(".engram.app.v1", "crate::app") .skip_debug([ ".engram.mcp.v1.PutSecretRequest", + ".engram.mcp.v1.CreatePolicySessionRequest", ".engram.mcp.v1.CompleteAuthorizationRequest", ".engram.mcp.v1.BeginAuthorizationResponse", ".engram.mcp.v1.LeaseResponse", ]) - .compile_protos(&["proto/engram/mcp/v1/control.proto"], &includes)?; + .compile_protos( + &[ + "proto/engram/mcp/v1/control.proto", + "proto/engram/mcp/v1/session_policy.proto", + ], + &includes, + )?; Ok(()) } diff --git a/crates/engram-protocol/proto/engram/app/v1/session.proto b/crates/engram-protocol/proto/engram/app/v1/session.proto index 3c7152dc5..073609c4e 100644 --- a/crates/engram-protocol/proto/engram/app/v1/session.proto +++ b/crates/engram-protocol/proto/engram/app/v1/session.proto @@ -25,6 +25,7 @@ service SessionService { // `tool_result_submitted` event at RPC time, enqueues a durable ToolResult // outbox row, and resumes an idle session exactly as SendPrompt does. rpc CompleteToolCall(CompleteToolCallRequest) returns (CompleteToolCallResponse); + rpc GetMcpWritePolicy(GetMcpWritePolicyRequest) returns (GetMcpWritePolicyResponse); // Streams the event envelope itself, not a wrapped Response message. // buf:lint:ignore RPC_RESPONSE_STANDARD_NAME rpc StreamEvents(StreamEventsRequest) returns (stream SessionEvent); @@ -317,6 +318,42 @@ message DequeueQueuedPromptResponse { string note = 2; } +message McpWritePolicyRef { + string session_id = 1; + int64 policy_revision = 2; + int64 binding_epoch = 3; +} +message McpTrustedWritePolicy { + McpWritePolicyRef ref = 1; + string write_eligibility = 2; + string cause = 3; + string accepted_input_id = 4; + optional string authenticated_actor_id = 5; + optional string prompt_id = 6; + optional string plan_tool_call_id = 7; + optional string plan_request_digest = 8; + repeated McpWritePolicyRef ancestor_policy_refs = 9; + bool current = 10; + bool initialized = 11; +} +message McpPlanReview { + McpWritePolicyRef policy_ref = 1; + string tool_call_id = 2; + string run_id = 3; + int64 event_idx = 4; + string request_digest = 5; + string args_json = 6; +} +message GetMcpWritePolicyRequest { + string session_id = 1; + optional string plan_tool_call_id = 2; +} +message GetMcpWritePolicyResponse { + McpTrustedWritePolicy policy = 1; + McpPlanReview plan_review = 2; + string reason = 3; + repeated string allowed_actions = 4; +} message CompleteToolCallRequest { string session_id = 1; // The call_id carried on the `tool_call_requested` event — the @@ -325,6 +362,8 @@ message CompleteToolCallRequest { // Opaque JSON result. Validated upstream (orchestrator) against the // tool's output schema; the coordinator never inspects it. string result_json = 3; + // Optional exact server review. Only the authenticated human handler consumes it. + McpPlanReview expected_mcp_plan_review = 4; } message CompleteToolCallResponse { diff --git a/crates/engram-protocol/proto/engram/mcp/v1/control.proto b/crates/engram-protocol/proto/engram/mcp/v1/control.proto index 1f1918b81..b1ef74ee4 100644 --- a/crates/engram-protocol/proto/engram/mcp/v1/control.proto +++ b/crates/engram-protocol/proto/engram/mcp/v1/control.proto @@ -2,13 +2,13 @@ syntax = "proto3"; package engram.mcp.v1; // Internal protocol 1. Served only on the separate mTLS listener. -service CredentialControl { - rpc PutSecret(PutSecretRequest) returns (CredentialStatus); +service CredentialControlService { + rpc PutSecret(PutSecretRequest) returns (PutSecretResponse); rpc BeginAuthorization(BeginAuthorizationRequest) returns (BeginAuthorizationResponse); rpc CancelAuthorization(CancelAuthorizationRequest) returns (CancelAuthorizationResponse); - rpc CompleteAuthorization(CompleteAuthorizationRequest) returns (CredentialStatus); - rpc GetStatus(GetStatusRequest) returns (CredentialStatus); - rpc Revoke(RevokeRequest) returns (CredentialStatus); + rpc CompleteAuthorization(CompleteAuthorizationRequest) returns (CompleteAuthorizationResponse); + rpc GetStatus(GetStatusRequest) returns (GetStatusResponse); + rpc Revoke(RevokeRequest) returns (RevokeResponse); rpc Lease(LeaseRequest) returns (LeaseResponse); } message Destination { @@ -162,3 +162,19 @@ message CancelAuthorizationResponse { string connection_id = 3; string registration_id = 4; } + +message PutSecretResponse { + CredentialStatus status = 1; +} + +message CompleteAuthorizationResponse { + CredentialStatus status = 1; +} + +message GetStatusResponse { + CredentialStatus status = 1; +} + +message RevokeResponse { + CredentialStatus status = 1; +} diff --git a/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto b/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto new file mode 100644 index 000000000..2f209ffb7 --- /dev/null +++ b/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; +package engram.mcp.v1; + +import "engram/app/v1/session.proto"; + +// Dedicated pinned mTLS only. Create reuses the ordinary creation core. +service SessionPolicyService { + rpc CreatePolicySession(CreatePolicySessionRequest) returns (CreatePolicySessionResponse); + rpc ReadWritePolicy(ReadWritePolicyRequest) returns (ReadWritePolicyResponse); + rpc AcceptPlanExit(AcceptPlanExitRequest) returns (AcceptPlanExitResponse); + rpc ValidateDispatchFence(ValidateDispatchFenceRequest) returns (ValidateDispatchFenceResponse); +} +message CreatePolicySessionRequest { + uint32 protocol = 1; + engram.app.v1.CreateSessionRequest create = 2; + string actor_id = 3; + string input_id = 4; + optional string parent_session_id = 5; +} +message ReadWritePolicyRequest { + uint32 protocol = 1; + string session_id = 2; + optional string plan_tool_call_id = 3; +} +message AcceptPlanExitRequest { + uint32 protocol = 1; + engram.app.v1.McpPlanReview expected = 2; + string actor_id = 3; + string result_json = 4; +} +message ValidateDispatchFenceRequest { + uint32 protocol = 1; + string session_id = 2; + string run_id = 3; + string tool_call_id = 4; + engram.app.v1.McpWritePolicyRef accepted_policy_ref = 5; + string accepted_write_eligibility = 6; + repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 7; +} +message ValidateDispatchFenceResponse { + uint32 protocol = 1; + string status = 2; +} + +message CreatePolicySessionResponse { + engram.app.v1.CreateSessionResponse session = 1; +} +message ReadWritePolicyResponse { + engram.app.v1.GetMcpWritePolicyResponse state = 1; +} +message AcceptPlanExitResponse { + engram.app.v1.McpTrustedWritePolicy policy = 1; +} diff --git a/crates/engram-protocol/src/mcp.rs b/crates/engram-protocol/src/mcp.rs index 619f4d3b2..a3dc0eea3 100644 --- a/crates/engram-protocol/src/mcp.rs +++ b/crates/engram-protocol/src/mcp.rs @@ -1,5 +1,5 @@ //! Internal MCP credential control protocol 1; no cleartext app registration. -tonic::include_proto!("engram.mcp.v1"); +include!(concat!(env!("OUT_DIR"), "/mcp/engram.mcp.v1.rs")); pub const PROTOCOL: u32 = 1; macro_rules! redacted_debug { ($($ty:ty),*) => {$ (impl std::fmt::Debug for $ty { @@ -8,6 +8,7 @@ macro_rules! redacted_debug { } redacted_debug!( PutSecretRequest, + CreatePolicySessionRequest, CompleteAuthorizationRequest, BeginAuthorizationResponse, LeaseResponse diff --git a/crates/engram-protocol/tests/mcp_policy_wire.rs b/crates/engram-protocol/tests/mcp_policy_wire.rs new file mode 100644 index 000000000..1f69cd6ce --- /dev/null +++ b/crates/engram-protocol/tests/mcp_policy_wire.rs @@ -0,0 +1,33 @@ +use engram_protocol::{app, mcp}; +use proptest::{prelude::*, test_runner::FileFailurePersistence}; +use prost::Message; + +#[test] +fn app_reflection_excludes_internal_policy_and_credential_services() { + let bytes = app::FILE_DESCRIPTOR_SET; + for private in [ + "engram/mcp/v1/session_policy.proto", + "SessionPolicyService", + "CredentialControlService", + ] { + assert!(!bytes + .windows(private.len()) + .any(|w| w == private.as_bytes())); + } +} +proptest! { + #![proptest_config(ProptestConfig { cases: 64, failure_persistence: Some(Box::new(FileFailurePersistence::Direct("proptest-regressions/mcp_policy_wire.txt"))), ..ProptestConfig::default() })] + #[test] + fn policy_decode_never_panics(bytes in proptest::collection::vec(any::(),0..=4096)) { + let _=mcp::CreatePolicySessionRequest::decode(bytes.as_slice()); + let _=mcp::ReadWritePolicyRequest::decode(bytes.as_slice()); + let _=mcp::AcceptPlanExitRequest::decode(bytes.as_slice()); + let _=mcp::ValidateDispatchFenceRequest::decode(bytes.as_slice()); + let _=app::CompleteToolCallRequest::decode(bytes.as_slice()); + let _=app::McpPlanReview::decode(bytes.as_slice()); + let _=mcp::PutSecretResponse::decode(bytes.as_slice()); + let _=mcp::CompleteAuthorizationResponse::decode(bytes.as_slice()); + let _=mcp::GetStatusResponse::decode(bytes.as_slice()); + let _=mcp::RevokeResponse::decode(bytes.as_slice()); + } +} diff --git a/crates/engram-sim/src/meta/mcp_policy.rs b/crates/engram-sim/src/meta/mcp_policy.rs new file mode 100644 index 000000000..b2b0bdad9 --- /dev/null +++ b/crates/engram-sim/src/meta/mcp_policy.rs @@ -0,0 +1,142 @@ +use super::{SimDb, SimMetadataStore}; +use chrono::{DateTime, Utc}; +use engram_core::{ + types::{event::PersistedEvent, mcp_policy::*, outbox::*}, + MetaError, SessionId, +}; +use std::collections::BTreeSet; +pub(super) fn nodes(db: &SimDb, id: SessionId) -> Result, MetaError> { + let mut result = Vec::new(); + let mut seen = BTreeSet::new(); + let mut next = Some(id); + while let Some(id) = next { + if result.len() >= 32 || !seen.insert(id) { + return Err(conflict("policy ancestry unavailable")); + } + let row = db.sessions.get(&id).ok_or(MetaError::NotFound)?; + result.push(PolicyNode { + id, + binding_epoch: row.binding_epoch, + parent: row.mcp_policy_parent_id, + policy: row.mcp_write_policy.clone(), + terminal: row.session.status.is_terminal(), + active: row.session.status == engram_core::types::session::SessionState::Active, + }); + next = row.mcp_policy_parent_id; + } + Ok(result) +} +pub(super) fn append( + db: &mut SimDb, + id: SessionId, + kind: &str, + payload: serde_json::Value, + now: DateTime, +) -> Result { + let row = db.sessions.get_mut(&id).ok_or(MetaError::NotFound)?; + let idx = row.next_event_idx; + row.next_event_idx += 1; + row.session.last_event_at = Some(now); + row.updated_at = now; + let recovery_epoch = row.recovery_epoch; + db.session_events + .entry(id) + .or_default() + .push(PersistedEvent { + idx, + kind: kind.into(), + payload, + created_at: now, + recovery_epoch, + rewound_at: None, + }); + Ok(idx) +} +impl SimMetadataStore { + pub(super) fn policy_read(&self, id: SessionId) -> Result { + self.gate()?; + compose_policy(&nodes(&self.db.lock(), id)?) + } + pub(super) fn policy_review(&self, id: SessionId, call: &str) -> Result { + self.gate()?; + let db = self.db.lock(); + let ns = nodes(&db, id)?; + plan_review( + &ns[0], + db.session_events.get(&id).map_or(&[], Vec::as_slice), + call, + ) + } + pub(super) fn policy_fence( + &self, + ctx: &RequestPolicyContext, + ) -> Result { + self.gate()?; + let db = self.db.lock(); + dispatch_fence( + &nodes(&db, ctx.session_id)?, + db.session_events + .get(&ctx.session_id) + .map_or(&[], Vec::as_slice), + ctx, + ) + } + pub(super) fn policy_accept( + &self, + input: &PlanAcceptance, + ) -> Result { + self.gate()?; + let id = input.review.policy_ref.session_id; + let now = self.now(); + let mut db = self.db.lock(); + let mut ns = nodes(&db, id)?; + let outbox_id = tool_result_outbox_id(id, &input.review.tool_call_id); + let payload = serde_json::json!({"tool_call_id":input.review.tool_call_id,"result_json":input.result_json,"mcp_policy_acceptance":input}); + if let Some(row) = db.outbox.get(&outbox_id) { + if row.session_id != id || row.kind != OutboxKind::ToolResult || row.payload != payload + { + return Err(conflict("plan result already accepted")); + } + return compose_policy(&ns); + } + let updated = accept_plan( + &ns[0], + db.session_events.get(&id).map_or(&[], Vec::as_slice), + input, + )?; + db.sessions + .get_mut(&id) + .ok_or(MetaError::NotFound)? + .mcp_write_policy = Some(updated.clone()); + db.outbox.insert( + outbox_id.clone(), + OutboxRow { + prompt_id: outbox_id, + session_id: id, + kind: OutboxKind::ToolResult, + payload, + created_at: now, + attempts: 0, + not_before: now, + delivered_at: None, + acked_at: None, + }, + ); + let idx = append( + &mut db, + id, + "tool_result_submitted", + serde_json::json!({"tool_call_id":input.review.tool_call_id,"result_json":input.result_json,"at":now}), + now, + )?; + ns[0].policy = Some(updated); + let p = compose_policy(&ns)?; + drop(db); + self.notify( + "session_events", + serde_json::json!({"session_id":id,"idx":idx}).to_string(), + ); + self.notify("session_outbox", id.to_string()); + Ok(p) + } +} diff --git a/crates/engram-sim/src/meta/mod.rs b/crates/engram-sim/src/meta/mod.rs index 018ccaf8c..9ae6c1c91 100644 --- a/crates/engram-sim/src/meta/mod.rs +++ b/crates/engram-sim/src/meta/mod.rs @@ -42,6 +42,7 @@ use engram_core::traits::{Clock, Entropy}; use engram_core::MetaError; use parking_lot::Mutex; +mod mcp_policy; mod store_impl; /// A would-be `pg_notify` recorded instead of fired. The D5 scheduler @@ -138,6 +139,8 @@ use engram_core::{HostId, SessionId, SnapshotId}; /// keeps that the domain type doesn't carry. #[derive(Clone, Debug)] pub struct SessRow { + pub mcp_write_policy: Option, + pub mcp_policy_parent_id: Option, pub nbd_slot_need: u32, pub session: Session, pub mem_budget_mib: i64, diff --git a/crates/engram-sim/src/meta/store_impl.rs b/crates/engram-sim/src/meta/store_impl.rs index bb449da3a..6cfc89700 100644 --- a/crates/engram-sim/src/meta/store_impl.rs +++ b/crates/engram-sim/src/meta/store_impl.rs @@ -709,6 +709,32 @@ impl MetadataStore for SimMetadataStore { // ================= sessions ================= + async fn read_mcp_write_policy( + &self, + id: SessionId, + ) -> Result { + self.policy_read(id) + } + async fn read_mcp_plan_review( + &self, + id: SessionId, + call: &str, + ) -> Result { + self.policy_review(id, call) + } + async fn accept_mcp_plan_exit( + &self, + input: &engram_core::types::mcp_policy::PlanAcceptance, + ) -> Result { + self.policy_accept(input) + } + async fn validate_mcp_dispatch_fence( + &self, + ctx: &engram_core::types::mcp_policy::RequestPolicyContext, + ) -> Result { + self.policy_fence(ctx) + } + /// `INSERT INTO sessions (id, status='pending', image_uri, mode, /// created_at, last_active_at) VALUES (...)`. async fn create_session(&self, spec: SessionSpec) -> Result { @@ -719,6 +745,8 @@ impl MetadataStore for SimMetadataStore { db.sessions.insert( id, SessRow { + mcp_write_policy: None, + mcp_policy_parent_id: None, nbd_slot_need: 2, session: Session { id, @@ -837,6 +865,20 @@ impl MetadataStore for SimMetadataStore { "session OAuth binding requires a live credential".into(), )); } + if db.sessions.contains_key(&ws.session_id) { + return Err(MetaError::Conflict("session already exists".into())); + } + if let Some(policy) = &ws.mcp_policy { + if policy.actor_id.is_empty() + || policy.input_id.is_empty() + || policy.parent_session_id == Some(ws.session_id) + { + return Err(MetaError::Conflict("invalid policy creation".into())); + } + if let Some(parent) = policy.parent_session_id { + super::mcp_policy::nodes(&db, parent)?; + } + } let picked = Self::pick_host_2d( &db, candidates, @@ -857,6 +899,11 @@ impl MetadataStore for SimMetadataStore { db.sessions.insert( ws.session_id, SessRow { + mcp_write_policy: ws + .mcp_policy + .as_ref() + .map(engram_core::types::mcp_policy::StoredWritePolicy::initial), + mcp_policy_parent_id: ws.mcp_policy.as_ref().and_then(|p| p.parent_session_id), nbd_slot_need: ws.nbd_slot_need, session: Session { id: ws.session_id, @@ -923,6 +970,16 @@ impl MetadataStore for SimMetadataStore { r.session.status == SessionState::Pending && r.session.sandbox_id.is_none() }); if remove { + // The immutable ancestry FK prevents orphaning a child policy. + if db + .sessions + .values() + .any(|row| row.mcp_policy_parent_id == Some(session_id)) + { + return Err(MetaError::Db(Box::new(std::io::Error::other( + "policy parent is referenced", + )))); + } db.sessions.remove(&session_id); } Ok(()) @@ -4054,26 +4111,29 @@ impl MetadataStore for SimMetadataStore { "outbox row session_id mismatch".into(), )); } - { - let mut db = self.db.lock(); - match db.outbox.get(&row.prompt_id) { - None => { - db.outbox.insert(row.prompt_id.clone(), row.clone()); - } - Some(existing) => { - if existing.session_id != row.session_id - || existing.kind != row.kind - || existing.payload != row.payload - { - return Err(MetaError::Conflict(format!( - "outbox id {} belongs to another command", - row.prompt_id - ))); - } - } + let mut db = self.db.lock(); + if !db.sessions.contains_key(&session_id) { + return Err(MetaError::NotFound); + } + if let Some(existing) = db.outbox.get(&row.prompt_id) { + if existing.session_id != row.session_id + || existing.kind != row.kind + || existing.payload != row.payload + { + return Err(MetaError::Conflict( + "outbox belongs to another command".into(), + )); } } - let idx = self.append_session_event(session_id, kind, payload).await?; + db.outbox + .entry(row.prompt_id.clone()) + .or_insert_with(|| row.clone()); + let idx = super::mcp_policy::append(&mut db, session_id, kind, payload, self.now())?; + drop(db); + self.notify( + "session_events", + serde_json::json!({"session_id":session_id,"idx":idx}).to_string(), + ); self.notify("session_outbox", session_id.to_string()); Ok(idx) } @@ -4084,44 +4144,58 @@ impl MetadataStore for SimMetadataStore { events: &[(String, serde_json::Value)], row: &engram_core::types::outbox::OutboxRow, ) -> Result>, MetaError> { - // Faithful mirror of the PG impl: a retry of the SAME command - // (existing prompt_id, matching identity) appends NOTHING and - // returns Ok(None); a prompt_id claimed by a DIFFERENT command - // is Conflict. Only a fresh insert appends the events. self.gate()?; if row.session_id != session_id { return Err(MetaError::Serialization( "outbox row session_id mismatch".into(), )); } - { - let mut db = self.db.lock(); - match db.outbox.get(&row.prompt_id) { - None => { - db.outbox.insert(row.prompt_id.clone(), row.clone()); - } - Some(existing) => { - // Payload is part of the command identity (review - // finding on #993, PG parity): a reused prompt_id - // with different text/mode conflicts, never drops. - if existing.session_id != row.session_id - || existing.kind != row.kind - || existing.payload != row.payload - { - return Err(MetaError::Conflict(format!( - "outbox id {} belongs to another command", - row.prompt_id - ))); - } - return Ok(None); - } + let mut db = self.db.lock(); + let session = db.sessions.get(&session_id).ok_or(MetaError::NotFound)?; + if let Some(existing) = db.outbox.get(&row.prompt_id) { + if existing.session_id != row.session_id + || existing.kind != row.kind + || existing.payload != row.payload + { + return Err(MetaError::Conflict( + "outbox belongs to another command".into(), + )); } + return Ok(None); } + let restriction = if row.kind == engram_core::types::outbox::OutboxKind::Prompt + && row.payload["mode"] == "plan" + { + Some(engram_core::types::mcp_policy::StoredWritePolicy::restrict( + session.mcp_write_policy.as_ref(), + &row.prompt_id, + session.next_event_idx, + )?) + } else { + None + }; + if let Some(policy) = restriction { + db.sessions + .get_mut(&session_id) + .ok_or(MetaError::NotFound)? + .mcp_write_policy = Some(policy); + } + db.outbox.insert(row.prompt_id.clone(), row.clone()); let mut idxs = Vec::with_capacity(events.len()); for (kind, payload) in events { - idxs.push( - self.append_session_event(session_id, kind, payload.clone()) - .await?, + idxs.push(super::mcp_policy::append( + &mut db, + session_id, + kind, + payload.clone(), + self.now(), + )?); + } + drop(db); + for idx in &idxs { + self.notify( + "session_events", + serde_json::json!({"session_id":session_id,"idx":idx}).to_string(), ); } self.notify("session_outbox", session_id.to_string()); diff --git a/crates/engram-sim/tests/meta_conformance.rs b/crates/engram-sim/tests/meta_conformance.rs index 86facb4f8..86fdbd2b6 100644 --- a/crates/engram-sim/tests/meta_conformance.rs +++ b/crates/engram-sim/tests/meta_conformance.rs @@ -1027,6 +1027,7 @@ async fn host_fc_snapshot_version(ctx: &Ctx) { async fn queue_fifo(ctx: &Ctx) { let meta = &ctx.meta; let enqueue = |sid: SessionId| engram_core::traits::metadata::SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id: sid, spec: spec("conf:q"), @@ -4158,6 +4159,7 @@ async fn stale_pending_reservation(ctx: &Ctx) { // A placed pending with a queued create_boot op. let with_op = SessionId::new(); let ws = |sid: SessionId| engram_core::traits::metadata::SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id: sid, spec: spec("conf:722"), @@ -5832,6 +5834,7 @@ async fn teleport_reservation_arm_counts_open_rows(ctx: &Ctx) { let mut t = teleport_fixture(ctx, source, h).await; let pending = SessionId::new(); let ws = engram_core::traits::metadata::SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id: pending, spec: spec("conf:placement"), @@ -6325,6 +6328,7 @@ async fn teleport_admission_fixture( } let sid = SessionId::new(); let ws = engram_core::traits::metadata::SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 1, session_id: sid, spec: spec("conf:teleport-machine"), @@ -7104,6 +7108,7 @@ async fn nbd_host_placement(ctx: &Ctx) { assert_eq!(read.utilization.nbd_slots_total, 4); assert_eq!(read.utilization.nbd_slots_in_use, 1); let ws = |need| SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: need, session_id: SessionId::new(), spec: spec("conf:slots"), @@ -7543,6 +7548,7 @@ async fn mcp_guest_binding_rejected(ctx: &Ctx) { .unwrap(); let sid = SessionId::from(uuid::Uuid::from_u128(4)); let ws = engram_core::traits::SessionCreateWriteSet { + mcp_policy: None, nbd_slot_need: 0, session_id: sid, spec: spec("test:mcp"), @@ -8044,3 +8050,603 @@ conformance!( t_mcp_flow_cancellation_fences_publication, mcp_flow_cancellation_fences_publication ); + +/// Accepted inputs change the server policy; guest events and queued defaults do not. +async fn mcp_write_policy_prompt_latch(ctx: &Ctx) { + let meta = &ctx.meta; + let id = meta + .create_session(spec("test.invalid/policy:latest")) + .await + .unwrap(); + let initial = meta.read_mcp_write_policy(id).await.unwrap(); + assert_eq!(initial.write_eligibility.as_str(), "unknown"); + let now = ctx.clock.now_utc(); + let mut row = engram_core::types::outbox::OutboxRow { + prompt_id: "policy-plan".into(), + session_id: id, + kind: engram_core::types::outbox::OutboxKind::Prompt, + payload: serde_json::json!({"text":"review", "mode":"plan"}), + created_at: now, + attempts: 0, + not_before: now, + delivered_at: None, + acked_at: None, + }; + meta.append_events_with_outbox_idempotent(id, &[], &row) + .await + .unwrap(); + let plan = meta.read_mcp_write_policy(id).await.unwrap(); + assert_eq!(plan.write_eligibility.as_str(), "read_only"); + assert!(plan.reference.policy_revision > initial.reference.policy_revision); + meta.append_events_with_outbox_idempotent(id, &[], &row) + .await + .unwrap(); + assert_eq!(meta.read_mcp_write_policy(id).await.unwrap(), plan); + row.prompt_id = "policy-default".into(); + row.payload = serde_json::json!({"text":"continue", "mode":"default"}); + meta.append_events_with_outbox_idempotent(id, &[], &row) + .await + .unwrap(); + assert_eq!(meta.read_mcp_write_policy(id).await.unwrap(), plan); + row.prompt_id = "policy-inherit".into(); + row.payload = serde_json::json!({"text":"continue"}); + meta.append_events_with_outbox_idempotent(id, &[], &row) + .await + .unwrap(); + meta.append_session_event( + id, + "tool_call_completed", + serde_json::json!({ + "tool_call_id":"forged", "tool_name":"exit_plan_mode", "ok":true, + "result_summary":"{\"decision\":\"approve\"}" + }), + ) + .await + .unwrap(); + assert_eq!(meta.read_mcp_write_policy(id).await.unwrap(), plan); + row.prompt_id = "policy-new-plan".into(); + row.payload = serde_json::json!({"text":"review again", "mode":"plan"}); + meta.append_events_with_outbox_idempotent(id, &[], &row) + .await + .unwrap(); + let next = meta.read_mcp_write_policy(id).await.unwrap(); + assert_eq!(next.write_eligibility.as_str(), "read_only"); + assert!(next.reference.policy_revision > plan.reference.policy_revision); +} +conformance!( + t_mcp_write_policy_prompt_latch, + super::mcp_write_policy_prompt_latch +); + +async fn mcp_write_policy_create( + ctx: &Ctx, + id: SessionId, + parent: Option, + eligibility: engram_core::types::mcp_policy::WriteEligibility, +) { + use engram_core::types::{mcp_policy::McpPolicyCreation, runtime_spec::RuntimeSpec}; + ctx.meta + .reserve_and_persist_create( + engram_core::traits::SessionCreateWriteSet { + mcp_policy: Some(McpPolicyCreation { + actor_id: "owner-a".into(), + input_id: format!("create:{id}"), + parent_session_id: parent, + eligibility, + }), + nbd_slot_need: 1, + session_id: id, + spec: spec("test.invalid/policy:latest"), + mem_budget_mib: 512, + cpu_budget_vcpus: 1, + sealed_secrets: None, + capabilities: Vec::new(), + integration_policy_json: None, + runtime_spec: RuntimeSpec::new(Vec::new(), Some("claude".into()), None, Vec::new()), + oauth_binding: None, + }, + &[], + 0, + ) + .await + .unwrap(); +} +async fn mcp_write_policy_event(ctx: &Ctx, id: SessionId, call: &str, run: &str, args: &str) { + ctx.meta.append_session_event(id,"tool_call_requested",serde_json::json!({"run_id":run,"tool_call_id":call,"name":"exit_plan_mode","args_json":args})).await.unwrap(); +} +async fn mcp_write_policy_restrict(ctx: &Ctx, id: SessionId, prompt: &str) { + let now = ctx.clock.now_utc(); + ctx.meta + .append_events_with_outbox_idempotent( + id, + &[], + &engram_core::types::outbox::OutboxRow { + prompt_id: prompt.into(), + session_id: id, + kind: engram_core::types::outbox::OutboxKind::Prompt, + payload: serde_json::json!({"text":"review","mode":"plan"}), + created_at: now, + attempts: 0, + not_before: now, + delivered_at: None, + acked_at: None, + }, + ) + .await + .unwrap(); +} +async fn mcp_write_policy_human_exit(ctx: &Ctx) { + use engram_core::types::mcp_policy::*; + let id = SessionId::new(); + mcp_write_policy_create(ctx, id, None, WriteEligibility::ReadOnly).await; + mcp_write_policy_event(ctx, id, "plan-a", "run-a", "{\"plan\":\"reviewed\"}").await; + let review = ctx.meta.read_mcp_plan_review(id, "plan-a").await.unwrap(); + let input = PlanAcceptance { + review: review.clone(), + actor_id: "owner-a".into(), + result_json: "{\"decision\":\"approve\"}".into(), + }; + let mut rejection = input.clone(); + rejection.result_json = "{\"decision\":\"reject\"}".into(); + assert!(ctx.meta.accept_mcp_plan_exit(&rejection).await.is_err()); + let mut changed = input.clone(); + changed.review.args_json = "{}".into(); + assert!(ctx.meta.accept_mcp_plan_exit(&changed).await.is_err()); + let before = ctx.meta.read_mcp_write_policy(id).await.unwrap(); + let accepted = ctx.meta.accept_mcp_plan_exit(&input).await.unwrap(); + assert_eq!(accepted.write_eligibility, WriteEligibility::WriteEligible); + assert!(accepted.reference.policy_revision > before.reference.policy_revision); + let outbox = ctx + .meta + .outbox_get(&engram_core::types::outbox::tool_result_outbox_id( + id, "plan-a", + )) + .await + .unwrap() + .unwrap(); + assert_eq!(outbox.payload["result_json"], input.result_json); + let events = ctx + .meta + .list_session_events_since(id, -1, 100) + .await + .unwrap(); + assert_eq!( + events + .iter() + .filter(|e| e.kind == "tool_result_submitted") + .count(), + 1 + ); + assert_eq!( + ctx.meta.accept_mcp_plan_exit(&input).await.unwrap(), + accepted + ); + assert_eq!( + ctx.meta + .list_session_events_since(id, -1, 100) + .await + .unwrap() + .len(), + events.len() + ); + mcp_write_policy_restrict(ctx, id, "new-plan").await; + let newer = ctx.meta.read_mcp_write_policy(id).await.unwrap(); + assert_eq!( + ctx.meta.accept_mcp_plan_exit(&input).await.unwrap(), + newer, + "retry cannot relax a newer plan" + ); + assert_eq!(newer.write_eligibility, WriteEligibility::ReadOnly); + mcp_write_policy_event(ctx, id, "plan-b", "run-b", "{}").await; + let stale = ctx.meta.read_mcp_plan_review(id, "plan-b").await.unwrap(); + mcp_write_policy_restrict(ctx, id, "newer-plan").await; + assert!(ctx + .meta + .accept_mcp_plan_exit(&PlanAcceptance { + review: stale, + ..input + }) + .await + .is_err()); + assert!( + ctx.meta.read_mcp_plan_review(id, "plan-b").await.is_err(), + "old call cannot authorize new restriction" + ); +} +conformance!( + t_mcp_write_policy_human_exit, + super::mcp_write_policy_human_exit +); +async fn mcp_write_policy_projections_and_fences(ctx: &Ctx) { + use engram_core::types::mcp_policy::*; + let parent = SessionId::new(); + let child = SessionId::new(); + mcp_write_policy_create(ctx, parent, None, WriteEligibility::ReadOnly).await; + mcp_write_policy_create(ctx, child, Some(parent), WriteEligibility::WriteEligible).await; + let pre = ctx.meta.read_mcp_write_policy(child).await.unwrap(); + assert_eq!(pre.write_eligibility, WriteEligibility::ReadOnly); + assert_eq!(pre.ancestor_policy_refs.len(), 1); + for status in [ + SessionState::Pending, + SessionState::Created, + SessionState::Active, + ] { + ctx.meta + .transition_session(child, status, BindingDisposition::Retain) + .await + .unwrap(); + } + ctx.meta + .append_session_event(child, "run_started", serde_json::json!({"run_id":"run-a"})) + .await + .unwrap(); + mcp_write_policy_event(ctx, child, "write-a", "run-a", "{}").await; + let frozen = RequestPolicyContext { + session_id: child, + run_id: "run-a".into(), + tool_call_id: "write-a".into(), + accepted_policy_ref: pre.reference, + accepted_write_eligibility: pre.write_eligibility, + ancestor_policy_refs: pre.ancestor_policy_refs, + }; + mcp_write_policy_event(ctx, parent, "plan-a", "run-a", "{}").await; + let review = ctx + .meta + .read_mcp_plan_review(parent, "plan-a") + .await + .unwrap(); + ctx.meta + .accept_mcp_plan_exit(&PlanAcceptance { + review, + actor_id: "owner-a".into(), + result_json: "{\"decision\":\"approve\"}".into(), + }) + .await + .unwrap(); + assert_eq!( + ctx.meta.validate_mcp_dispatch_fence(&frozen).await.unwrap(), + DispatchFence::Unknown, + "earlier read-only request cannot gain writes" + ); + let p = ctx.meta.read_mcp_write_policy(child).await.unwrap(); + assert_eq!(p.write_eligibility, WriteEligibility::WriteEligible); + let current = RequestPolicyContext { + accepted_policy_ref: p.reference, + accepted_write_eligibility: p.write_eligibility, + ancestor_policy_refs: p.ancestor_policy_refs, + ..frozen + }; + assert_eq!( + ctx.meta + .validate_mcp_dispatch_fence(¤t) + .await + .unwrap(), + DispatchFence::Current + ); + let mut forged = current.clone(); + forged.run_id = "other-run".into(); + assert_eq!( + ctx.meta.validate_mcp_dispatch_fence(&forged).await.unwrap(), + DispatchFence::Stale + ); + forged = current.clone(); + forged.ancestor_policy_refs.clear(); + assert_eq!( + ctx.meta.validate_mcp_dispatch_fence(&forged).await.unwrap(), + DispatchFence::Stale + ); + mcp_write_policy_restrict(ctx, parent, "parent-plan").await; + assert_ne!( + ctx.meta + .validate_mcp_dispatch_fence(¤t) + .await + .unwrap(), + DispatchFence::Current + ); + assert_eq!( + ctx.meta + .read_mcp_write_policy(child) + .await + .unwrap() + .write_eligibility, + WriteEligibility::ReadOnly + ); + let legacy = ctx + .meta + .create_session(spec("test.invalid/legacy:latest")) + .await + .unwrap(); + mcp_write_policy_restrict(ctx, legacy, "legacy-plan").await; + mcp_write_policy_event(ctx, legacy, "legacy-call", "run-a", "{}").await; + assert!(ctx + .meta + .read_mcp_plan_review(legacy, "legacy-call") + .await + .is_err()); + let id = SessionId::new(); + mcp_write_policy_create(ctx, id, None, WriteEligibility::ReadOnly).await; + mcp_write_policy_event(ctx, id, "plan", "run", "{}").await; + let reviewed = ctx.meta.read_mcp_plan_review(id, "plan").await.unwrap(); + mcp_write_policy_event(ctx, id, "plan", "run", "{\"changed\":true}").await; + assert!(ctx + .meta + .accept_mcp_plan_exit(&PlanAcceptance { + review: reviewed, + actor_id: "owner-a".into(), + result_json: "{\"decision\":\"approve\"}".into() + }) + .await + .is_err()); +} +conformance!( + t_mcp_write_policy_projections_and_fences, + super::mcp_write_policy_projections_and_fences +); + +async fn mcp_write_policy_restore_and_generic_results(ctx: &Ctx) { + use engram_core::types::mcp_policy::*; + let id = SessionId::new(); + mcp_write_policy_create(ctx, id, None, WriteEligibility::ReadOnly).await; + mcp_write_policy_event(ctx, id, "automatic-plan", "run-a", "{}").await; + let now = ctx.clock.now_utc(); + let row = engram_core::types::outbox::OutboxRow { + prompt_id: engram_core::types::outbox::tool_result_outbox_id(id, "automatic-plan"), + session_id: id, + kind: engram_core::types::outbox::OutboxKind::ToolResult, + payload: serde_json::json!({"tool_call_id":"automatic-plan","result_json":"{\"decision\":\"approve\"}"}), + created_at: now, + attempts: 0, + not_before: now, + delivered_at: None, + acked_at: None, + }; + ctx.meta.append_session_event_and_outbox(id,"tool_result_submitted",serde_json::json!({"tool_call_id":"automatic-plan","result_json":"{\"decision\":\"approve\"}"}),&row).await.unwrap(); + assert_eq!( + ctx.meta + .read_mcp_write_policy(id) + .await + .unwrap() + .write_eligibility, + WriteEligibility::ReadOnly + ); + assert!(ctx + .meta + .read_mcp_plan_review(id, "automatic-plan") + .await + .is_err()); + mcp_write_policy_event(ctx, id, "restored-plan", "run-a", "{}").await; + let review = ctx + .meta + .read_mcp_plan_review(id, "restored-plan") + .await + .unwrap(); + ctx.meta.rewind_session_to_cursor(id, -1).await.unwrap(); + assert!(ctx + .meta + .accept_mcp_plan_exit(&PlanAcceptance { + review, + actor_id: "owner-a".into(), + result_json: "{\"decision\":\"approve\"}".into() + }) + .await + .is_err()); + assert_eq!( + ctx.meta + .read_mcp_write_policy(id) + .await + .unwrap() + .write_eligibility, + WriteEligibility::ReadOnly + ); + mcp_write_policy_event(ctx, id, "current-plan", "run-b", "{}").await; + let review = ctx + .meta + .read_mcp_plan_review(id, "current-plan") + .await + .unwrap(); + ctx.meta + .accept_mcp_plan_exit(&PlanAcceptance { + review, + actor_id: "owner-a".into(), + result_json: "{\"decision\":\"approve\"}".into(), + }) + .await + .unwrap(); + for status in [ + SessionState::Pending, + SessionState::Created, + SessionState::Active, + ] { + ctx.meta + .transition_session(id, status, BindingDisposition::Retain) + .await + .unwrap(); + } + ctx.meta + .append_session_event(id, "run_started", serde_json::json!({"run_id":"run-b"})) + .await + .unwrap(); + mcp_write_policy_event(ctx, id, "write-b", "run-b", "{}").await; + let p = ctx.meta.read_mcp_write_policy(id).await.unwrap(); + let frozen = RequestPolicyContext { + session_id: id, + run_id: "run-b".into(), + tool_call_id: "write-b".into(), + accepted_policy_ref: p.reference.clone(), + accepted_write_eligibility: p.write_eligibility, + ancestor_policy_refs: p.ancestor_policy_refs.clone(), + }; + assert_eq!( + ctx.meta.validate_mcp_dispatch_fence(&frozen).await.unwrap(), + DispatchFence::Current + ); + ctx.meta + .fenced_assign_sandbox(id, 0, Some(engram_core::SandboxId::new()), None) + .await + .unwrap(); + assert_eq!( + ctx.meta.validate_mcp_dispatch_fence(&frozen).await.unwrap(), + DispatchFence::Stale + ); + let restored = ctx.meta.read_mcp_write_policy(id).await.unwrap(); + assert_eq!( + restored.reference.policy_revision, + p.reference.policy_revision + ); + assert_eq!(restored.write_eligibility, WriteEligibility::WriteEligible); + if let Some(db) = &ctx._pg { + let reopened = engram_postgres::PostgresStore::connect(&db.url) + .await + .unwrap() + .with_clock(ctx.clock.clone()); + assert_eq!( + reopened.read_mcp_write_policy(id).await.unwrap(), + restored, + "new connection reads durable policy after restore" + ); + } +} +conformance!( + t_mcp_write_policy_restore_and_generic_results, + super::mcp_write_policy_restore_and_generic_results +); + +async fn mcp_write_policy_restriction_race(ctx: &Ctx) { + use engram_core::types::mcp_policy::*; + let id = SessionId::new(); + mcp_write_policy_create(ctx, id, None, WriteEligibility::WriteEligible).await; + for status in [ + SessionState::Pending, + SessionState::Created, + SessionState::Active, + ] { + ctx.meta + .transition_session(id, status, BindingDisposition::Retain) + .await + .unwrap(); + } + ctx.meta + .append_session_event(id, "run_started", serde_json::json!({"run_id":"run-a"})) + .await + .unwrap(); + mcp_write_policy_event(ctx, id, "call-a", "run-a", "{}").await; + let p = ctx.meta.read_mcp_write_policy(id).await.unwrap(); + let frozen = RequestPolicyContext { + session_id: id, + run_id: "run-a".into(), + tool_call_id: "call-a".into(), + accepted_policy_ref: p.reference, + accepted_write_eligibility: p.write_eligibility, + ancestor_policy_refs: p.ancestor_policy_refs, + }; + let (fence, ()) = tokio::join!( + ctx.meta.validate_mcp_dispatch_fence(&frozen), + mcp_write_policy_restrict(ctx, id, "race-plan") + ); + assert!( + matches!( + fence.unwrap(), + DispatchFence::Current | DispatchFence::Unknown + ), + "the lock defines which operation is admitted first" + ); + assert_ne!( + ctx.meta.validate_mcp_dispatch_fence(&frozen).await.unwrap(), + DispatchFence::Current, + "a committed restriction blocks every later fence" + ); +} +conformance!( + t_mcp_write_policy_restriction_race, + super::mcp_write_policy_restriction_race +); + +async fn mcp_write_policy_history_bound(ctx: &Ctx) { + use engram_core::types::mcp_policy::WriteEligibility; + let id = SessionId::new(); + mcp_write_policy_create(ctx, id, None, WriteEligibility::ReadOnly).await; + ctx.meta + .append_session_event(id, "run_started", serde_json::json!({"run_id":"run-bound"})) + .await + .unwrap(); + let request = serde_json::json!({"run_id":"run-bound","tool_call_id":"bounded-plan","name":"exit_plan_mode","args_json":"{}"}); + let events: Vec<_> = (0..1023) + .map(|_| ("tool_call_requested".to_string(), request.clone())) + .collect(); + let now = ctx.clock.now_utc(); + ctx.meta + .append_events_with_outbox_idempotent( + id, + &events, + &engram_core::types::outbox::OutboxRow { + prompt_id: "bounded-request-input".into(), + session_id: id, + kind: engram_core::types::outbox::OutboxKind::Prompt, + payload: serde_json::json!({"text":"test history"}), + created_at: now, + attempts: 0, + not_before: now, + delivered_at: None, + acked_at: None, + }, + ) + .await + .unwrap(); + assert!(ctx + .meta + .read_mcp_plan_review(id, "bounded-plan") + .await + .is_ok()); + ctx.meta + .append_session_event(id, "tool_call_requested", request) + .await + .unwrap(); + assert!(ctx + .meta + .read_mcp_plan_review(id, "bounded-plan") + .await + .is_err()); +} +conformance!( + t_mcp_write_policy_history_bound, + super::mcp_write_policy_history_bound +); + +async fn mcp_write_policy_parent_lifecycle(ctx: &Ctx) { + use engram_core::types::mcp_policy::WriteEligibility; + let parent = SessionId::new(); + let child = SessionId::new(); + mcp_write_policy_create(ctx, parent, None, WriteEligibility::WriteEligible).await; + mcp_write_policy_create(ctx, child, Some(parent), WriteEligibility::WriteEligible).await; + ctx.meta + .transition_session(parent, SessionState::Pending, BindingDisposition::Retain) + .await + .unwrap(); + assert!( + matches!( + ctx.meta.delete_pending_session(parent).await, + Err(engram_core::MetaError::Db(_)) + ), + "immutable parent FK must preserve ancestry" + ); + assert_eq!( + ctx.meta + .read_mcp_write_policy(child) + .await + .unwrap() + .write_eligibility, + WriteEligibility::WriteEligible + ); + ctx.meta + .transition_session(parent, SessionState::Failed, BindingDisposition::Retain) + .await + .unwrap(); + let policy = ctx.meta.read_mcp_write_policy(child).await.unwrap(); + assert_eq!(policy.write_eligibility, WriteEligibility::Unknown); + assert!(!policy.current); + assert_eq!(policy.ancestor_policy_refs.len(), 1); +} +conformance!( + t_mcp_write_policy_parent_lifecycle, + super::mcp_write_policy_parent_lifecycle +); diff --git a/crates/engram-tui/src/net.rs b/crates/engram-tui/src/net.rs index ac629cdf3..f686e59a7 100644 --- a/crates/engram-tui/src/net.rs +++ b/crates/engram-tui/src/net.rs @@ -317,6 +317,7 @@ impl Net { tokio::spawn(async move { let res = c .complete_tool_call(CompleteToolCallRequest { + expected_mcp_plan_review: None, session_id: session_id.clone(), tool_call_id: tool_call_id.clone(), result_json, diff --git a/deploy/migrations/0128_mcp_session_write_policy.sql b/deploy/migrations/0128_mcp_session_write_policy.sql new file mode 100644 index 000000000..89cf812bf --- /dev/null +++ b/deploy/migrations/0128_mcp_session_write_policy.sql @@ -0,0 +1,9 @@ +-- Nullable policy preserves unknown legacy authority. Parent identity is immutable. +ALTER TABLE sessions ADD COLUMN mcp_write_policy JSONB + CHECK (mcp_write_policy IS NULL OR jsonb_typeof(mcp_write_policy) = 'object'); +ALTER TABLE sessions ADD COLUMN mcp_policy_parent_id UUID REFERENCES sessions(id); +ALTER TABLE sessions ADD CONSTRAINT mcp_policy_no_self_parent CHECK (mcp_policy_parent_id <> id); +CREATE INDEX sessions_mcp_policy_parent_idx ON sessions(mcp_policy_parent_id) + WHERE mcp_policy_parent_id IS NOT NULL; +CREATE INDEX session_events_tool_call_idx ON session_events(session_id, (payload->>'tool_call_id'), idx) + WHERE kind IN ('tool_call_requested', 'tool_result_submitted', 'tool_call_completed'); diff --git a/docs/adr/0125-isolated-external-mcp.md b/docs/adr/0125-isolated-external-mcp.md index f45167392..541b5127b 100644 --- a/docs/adr/0125-isolated-external-mcp.md +++ b/docs/adr/0125-isolated-external-mcp.md @@ -36,7 +36,10 @@ credential on its own. The coordinator is the only credential and refresh authority. The worker receives secrets only after admission, near dispatch. The coordinator exposes credential control only on a separate, default-off mTLS -listener. Exact client certificate pins identify the trusted orchestrator role. +listener. The internal service name is `engram.mcp.v1.CredentialControlService`. +Its four status operations return method-specific response wrappers with one +`CredentialStatus` field. Clients reject a missing status. The unreleased service +has no alias for the earlier name. Exact client certificate pins identify the trusted orchestrator role. The existing cleartext application gRPC listener never serves this service. The orchestrator may hold a short-lived credential lease in memory for one admitted worker operation. It must not queue, cache, log, persist, or return the value to a @@ -156,6 +159,48 @@ A lost begin response can leave an unknown pending flow until expiry and cleanup the stale orchestrator call cannot return its authorization URL or record a usable callback after recovery. Unknown flow IDs are never inferred or broadly cancelled. +### Trusted session write policy + +The coordinator owns a durable conservative MCP write-policy latch. This is server +permission, not proof of the guest mode. A trusted plan input sets read-only and +advances the revision in the existing prompt/outbox transaction. A later queued +default or absent directive cannot relax it. This can keep external writes blocked +after a default prompt starts locally. The session state explains the restriction; +the feature stays disabled until the UI presents the required review action. + +Only actual new creation over the dedicated pinned mTLS session-policy service can +initialize known policy, inside the existing creation write-set and after validation +of supported harness semantics. Legacy and generic creation remain unknown. The +initial plan restriction is part of creation, not its best-effort initial prompt. +An immutable parent link makes reads and dispatch intersect all current ancestors. +Missing ancestry fails closed. Durable policy survives restart and disk restore; +binding-epoch changes invalidate old request references. + +A human owner or administrator can relax a known restrictive policy only by accepting +the exact pending plan exit, bound to the restrictive revision, binding epoch, and +first durable requested-call payload. Changed duplicates, rewound requests, already +submitted/completed calls, and old plan requests cannot authorize relaxation. Fresh +orchestrator identity and ownership checks precede a pinned mTLS acceptance. The +coordinator commits the policy decision, submitted event and existing result outbox +in one transaction. Public CompleteToolCall carries additive expected review context; +without it, ordinary delivery is unchanged and cannot relax MCP policy. Automation, +guest completion, run-start, mode-shaped strings and disk stamps never relax policy. +The session-policy service shares the dedicated listener and client pins, with a +separate bounded create message budget; credential control keeps its 32 KiB limit. + +Request acceptance freezes eligibility and exact leaf/ancestor policy references. +An earlier read-only or unknown request cannot gain writes after relaxation. A stale +eligible request cannot rebase. After the orchestrator commits its irreversible +journal barrier, it must synchronously validate those references, the complete +stored ancestry, current binding and execution identity at the coordinator. The +coordinator locks all involved session rows in stable order. Its successful locked +check is the policy-admission ordering point. A restriction before that point blocks +the operation; one after it applies to already-admitted active I/O, canceled best +effort. This is not an atomic transaction across the two databases. Live orchestrator +permissions and journal admission are separate checks. A lost reply or crash after +the journal barrier never permits automatic write replay. Plan approval does not +replace the separate exact external-write approval or explicit automation grant. + ### Authorization and irreversible dispatch Before execution, validate the active session/run, immutable binding, principal, diff --git a/mcp-worker/README.md b/mcp-worker/README.md index 6aa66d371..19cb520b0 100644 --- a/mcp-worker/README.md +++ b/mcp-worker/README.md @@ -144,3 +144,9 @@ policy widens these routes, that the CNI enforces them, and that service NAT doe not bypass the intended exclusions. Rendered YAML and local TLS tests do not prove effective cluster isolation or Firecracker guest isolation. Those release gates remain required before dispatch can be enabled. + +The coordinator credential API uses `engram.mcp.v1.CredentialControlService` on +the pinned mTLS listener. Its status responses wrap `CredentialStatus`; a missing +status is an error. The credential API remains limited to 32 KiB. The separate +`engram.mcp.v1.SessionPolicyService` uses a 4 MiB create budget on that same +listener. Neither service is on the application listener or in its reflection. diff --git a/orchestrator/src/__tests__/mcp-control.test.ts b/orchestrator/src/__tests__/mcp-control.test.ts index 62b940e9e..27fc34090 100644 --- a/orchestrator/src/__tests__/mcp-control.test.ts +++ b/orchestrator/src/__tests__/mcp-control.test.ts @@ -1,3 +1,4 @@ +import { createClient, createRouterTransport } from "@connectrpc/connect"; import { expect, test } from "bun:test"; import { createSecureServer } from "node:http2"; import { execFileSync } from "node:child_process"; @@ -6,8 +7,14 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { X509Certificate } from "node:crypto"; import { connectNodeAdapter } from "@connectrpc/connect-node"; -import { CredentialControl } from "../gen/engram/mcp/v1/control_pb.ts"; -import { makeCredentialControlClient } from "../mcp/control-client.ts"; +import { CredentialControlService } from "../gen/engram/mcp/v1/control_pb.ts"; +import { + makeCredentialControlClient, + unwrapCredentialControlClient, + makeSessionPolicyControlClient, +} from "../mcp/control-client.ts"; + +import { SessionPolicyService } from "../gen/engram/mcp/v1/session_policy_pb.ts"; function certificates() { const dir = mkdtempSync(join(tmpdir(), "mcp-control-test-")); @@ -53,11 +60,27 @@ test("control client requires pinned mTLS and preserves int64 metadata", async ( { key: tls.key, cert: tls.cert, ca: tls.ca, requestCert: true, rejectUnauthorized: true }, connectNodeAdapter({ routes(router) { - router.service(CredentialControl, { + router.service(SessionPolicyService, { + async readWritePolicy(req) { + return { + state: { + policy: { + ref: { + sessionId: req.sessionId, + policyRevision: 9007199254740993n, + bindingEpoch: 9007199254740995n, + }, + writeEligibility: "read_only", + }, + }, + }; + }, + }); + router.service(CredentialControlService, { async getStatus(req) { calls++; expect(req.protocol).toBe(1); - return { protocol: 1, version: 9007199254740993n, status: "connected" }; + return { status: { protocol: 1, version: 9007199254740993n, status: "connected" } }; }, }); }, @@ -77,6 +100,17 @@ test("control client requires pinned mTLS and preserves int64 metadata", async ( expect((await good.getStatus({ protocol: 1, connectionId: "c" })).version).toBe( 9007199254740993n, ); + const policy = makeSessionPolicyControlClient({ url, ...tls }); + expect( + (await policy.readWritePolicy({ protocol: 1, sessionId: "s" })).state?.policy?.ref + ?.policyRevision, + ).toBe(9007199254740993n); + const badPolicy = makeSessionPolicyControlClient({ + url, + ...tls, + serverFingerprint: "00:".repeat(31) + "00", + }); + await expect(badPolicy.readWritePolicy({ protocol: 1 })).rejects.toThrow("control unavailable"); const bad = makeCredentialControlClient({ url, ...tls, @@ -103,3 +137,31 @@ test("control client requires pinned mTLS and preserves int64 metadata", async ( await new Promise((r) => server.close(() => r())); } }); + +test("credential response wrappers fail closed when status is absent", async () => { + const client = unwrapCredentialControlClient( + createClient( + CredentialControlService, + createRouterTransport((router) => + router.service(CredentialControlService, { + async putSecret() { + return {}; + }, + async completeAuthorization() { + return {}; + }, + async getStatus() { + return {}; + }, + async revoke() { + return {}; + }, + }), + ), + ), + ); + await expect(client.putSecret({})).rejects.toThrow("status unavailable"); + await expect(client.completeAuthorization({})).rejects.toThrow("status unavailable"); + await expect(client.getStatus({})).rejects.toThrow("status unavailable"); + await expect(client.revoke({})).rejects.toThrow("status unavailable"); +}); diff --git a/orchestrator/src/__tests__/mcp-credential-admin.test.ts b/orchestrator/src/__tests__/mcp-credential-admin.test.ts index dbc55c6ae..655624146 100644 --- a/orchestrator/src/__tests__/mcp-credential-admin.test.ts +++ b/orchestrator/src/__tests__/mcp-credential-admin.test.ts @@ -1,3 +1,4 @@ +import { unwrapCredentialControlClient } from "../mcp/control-client.ts"; import { expect, test } from "bun:test"; import { createClient, createRouterTransport } from "@connectrpc/connect"; import { create } from "@bufbuild/protobuf"; @@ -6,7 +7,7 @@ import { makeIntegrationConnectionStore } from "../db/integration-connections.ts import { makeCredentialMetadataStore } from "../mcp/credential-store.ts"; import { makeCredentialAdmin } from "../mcp/credential-admin.ts"; import { - CredentialControl, + CredentialControlService, CredentialKind, CredentialStatusSchema, type CredentialStatus, @@ -30,56 +31,61 @@ async function fixture() { let failRevoke = false, lostRevoke = false; const calls: string[] = []; - const control = createClient( - CredentialControl, - createRouterTransport((router) => - router.service(CredentialControl, { - async putSecret(req) { - calls.push("put"); - current = create(CredentialStatusSchema, { - protocol: 1, - context: { - connectionId: req.context!.connectionId, - configRevision: req.context!.configRevision, - resource: req.context!.resource, - authorizationGeneration: req.context!.previousAuthorizationGeneration + 1n, - credentialIdentity: `identity-${calls.length}`, - policyDigest: "a".repeat(64), - policyRevision: "v1", - }, - reference: { - kind: req.kind, - name: req.expected?.name || "mcp.fresh", - connectionId: row.id, - generation: (req.expected?.generation ?? 0n) + 1n, - }, - version: (req.expected?.generation ?? 0n) + 1n, - status: "connected", - }); - return current; - }, - async getStatus() { - if (!current) throw new Error("absent"); - return current; - }, - async revoke() { - calls.push("revoke"); - if (failRevoke) throw new Error("refused"); - current = create(CredentialStatusSchema, { - ...current!, - context: { - ...current!.context!, - authorizationGeneration: current!.context!.authorizationGeneration + 1n, - credentialIdentity: "revoked-identity", - }, - reference: { ...current!.reference!, generation: current!.reference!.generation + 1n }, - version: current!.version + 1n, - status: "revoked", - }); - if (lostRevoke) throw new Error("lost reply"); - return current; - }, - }), + const control = unwrapCredentialControlClient( + createClient( + CredentialControlService, + createRouterTransport((router) => + router.service(CredentialControlService, { + async putSecret(req) { + calls.push("put"); + current = create(CredentialStatusSchema, { + protocol: 1, + context: { + connectionId: req.context!.connectionId, + configRevision: req.context!.configRevision, + resource: req.context!.resource, + authorizationGeneration: req.context!.previousAuthorizationGeneration + 1n, + credentialIdentity: `identity-${calls.length}`, + policyDigest: "a".repeat(64), + policyRevision: "v1", + }, + reference: { + kind: req.kind, + name: req.expected?.name || "mcp.fresh", + connectionId: row.id, + generation: (req.expected?.generation ?? 0n) + 1n, + }, + version: (req.expected?.generation ?? 0n) + 1n, + status: "connected", + }); + return { status: current }; + }, + async getStatus() { + if (!current) throw new Error("absent"); + return { status: current }; + }, + async revoke() { + calls.push("revoke"); + if (failRevoke) throw new Error("refused"); + current = create(CredentialStatusSchema, { + ...current!, + context: { + ...current!.context!, + authorizationGeneration: current!.context!.authorizationGeneration + 1n, + credentialIdentity: "revoked-identity", + }, + reference: { + ...current!.reference!, + generation: current!.reference!.generation + 1n, + }, + version: current!.version + 1n, + status: "revoked", + }); + if (lostRevoke) throw new Error("lost reply"); + return { status: current }; + }, + }), + ), ), ); const admin = makeCredentialAdmin({ store, control, registrations: [], now: () => new Date() }); diff --git a/orchestrator/src/__tests__/mcp-credential-broker.test.ts b/orchestrator/src/__tests__/mcp-credential-broker.test.ts index 17f52846b..02ddee08e 100644 --- a/orchestrator/src/__tests__/mcp-credential-broker.test.ts +++ b/orchestrator/src/__tests__/mcp-credential-broker.test.ts @@ -1,8 +1,9 @@ +import { unwrapCredentialControlClient } from "../mcp/control-client.ts"; import { expect, test } from "bun:test"; import { createClient, createRouterTransport } from "@connectrpc/connect"; import { create } from "@bufbuild/protobuf"; import { - CredentialControl, + CredentialControlService, CredentialKind, CredentialStatusSchema, } from "../gen/engram/mcp/v1/control_pb.ts"; @@ -40,24 +41,26 @@ function fixture(expire = false) { deliveries = 0, leases = 0; const seen: unknown[] = []; - const control = createClient( - CredentialControl, - createRouterTransport((router) => - router.service(CredentialControl, { - async lease(req) { - leases++; - seen.push(req); - if (expire) time = now + 10000; - return { - protocol: 1, - header: "x-api-key", - value: "transient-value", - expiresAt: req.expiresAt, - operationExpiresAt: req.operationExpiresAt, - intentId: req.intentId, - }; - }, - }), + const control = unwrapCredentialControlClient( + createClient( + CredentialControlService, + createRouterTransport((router) => + router.service(CredentialControlService, { + async lease(req) { + leases++; + seen.push(req); + if (expire) time = now + 10000; + return { + protocol: 1, + header: "x-api-key", + value: "transient-value", + expiresAt: req.expiresAt, + operationExpiresAt: req.operationExpiresAt, + intentId: req.intentId, + }; + }, + }), + ), ), ); const broker = makeCredentialBroker({ @@ -143,15 +146,17 @@ test("worker policy and boot mismatch prevents secret lease", async () => { test("admission rejection prevents worker and credential access", async () => { const f = fixture(); let touched = false; - const control = createClient( - CredentialControl, - createRouterTransport((router) => - router.service(CredentialControl, { - async lease() { - touched = true; - throw new Error("unexpected"); - }, - }), + const control = unwrapCredentialControlClient( + createClient( + CredentialControlService, + createRouterTransport((router) => + router.service(CredentialControlService, { + async lease() { + touched = true; + throw new Error("unexpected"); + }, + }), + ), ), ); const broker = makeCredentialBroker({ @@ -198,21 +203,23 @@ test("authority invalidated after lease is checked again before resource dispatc const f = fixture(); let checks = 0, sent = false; - const control = createClient( - CredentialControl, - createRouterTransport((router) => - router.service(CredentialControl, { - async lease(req) { - return { - protocol: 1, - header: "x-api-key", - value: "secret", - expiresAt: req.expiresAt, - operationExpiresAt: req.operationExpiresAt, - intentId: req.intentId, - }; - }, - }), + const control = unwrapCredentialControlClient( + createClient( + CredentialControlService, + createRouterTransport((router) => + router.service(CredentialControlService, { + async lease(req) { + return { + protocol: 1, + header: "x-api-key", + value: "secret", + expiresAt: req.expiresAt, + operationExpiresAt: req.operationExpiresAt, + intentId: req.intentId, + }; + }, + }), + ), ), ); const broker = makeCredentialBroker({ @@ -247,21 +254,23 @@ test("authority invalidated after lease is checked again before resource dispatc }); test("worker transport errors cannot expose the transient credential", async () => { const f = fixture(); - const control = createClient( - CredentialControl, - createRouterTransport((router) => - router.service(CredentialControl, { - async lease(req) { - return { - protocol: 1, - header: "x-api-key", - value: "secret-value", - expiresAt: req.expiresAt, - operationExpiresAt: req.operationExpiresAt, - intentId: req.intentId, - }; - }, - }), + const control = unwrapCredentialControlClient( + createClient( + CredentialControlService, + createRouterTransport((router) => + router.service(CredentialControlService, { + async lease(req) { + return { + protocol: 1, + header: "x-api-key", + value: "secret-value", + expiresAt: req.expiresAt, + operationExpiresAt: req.operationExpiresAt, + intentId: req.intentId, + }; + }, + }), + ), ), ); const broker = makeCredentialBroker({ diff --git a/orchestrator/src/__tests__/mcp-credential-oauth.test.ts b/orchestrator/src/__tests__/mcp-credential-oauth.test.ts index 5910086c0..f5e1a3536 100644 --- a/orchestrator/src/__tests__/mcp-credential-oauth.test.ts +++ b/orchestrator/src/__tests__/mcp-credential-oauth.test.ts @@ -1,3 +1,4 @@ +import { unwrapCredentialControlClient } from "../mcp/control-client.ts"; import { expect, test } from "bun:test"; import { Code, ConnectError, createClient, createRouterTransport } from "@connectrpc/connect"; import { create } from "@bufbuild/protobuf"; @@ -6,7 +7,7 @@ import { makeIntegrationConnectionStore } from "../db/integration-connections.ts import { makeCredentialMetadataStore } from "../mcp/credential-store.ts"; import { makeCredentialAdmin } from "../mcp/credential-admin.ts"; import { - CredentialControl, + CredentialControlService, CredentialKind, CredentialStatusSchema, type AuthorityContext, @@ -42,76 +43,78 @@ async function fixture() { let pauseComplete: (() => Promise) | undefined; let pauseBegin: (() => Promise) | undefined; let pauseStatus: (() => Promise) | undefined; - const control = createClient( - CredentialControl, - createRouterTransport((router) => - router.service(CredentialControl, { - async getStatus() { - if (outage) throw new ConnectError("outage", Code.Unavailable); - if (!current) throw new ConnectError("absent", Code.NotFound); - return current; - }, - async cancelAuthorization(req) { - if (outage) throw new ConnectError("outage", Code.Unavailable); - expect(req.connectionId).toBe(row.id); - cancelled.add(req.flowId); - return { - protocol: 1, - flowId: req.flowId, - connectionId: req.connectionId, - registrationId: req.registrationId, - }; - }, - async beginAuthorization(req) { - calls.push("begin"); - expect(req.sharingConsent).toBe(true); - expect(req.clientSecret).toBeUndefined(); - expect(req.registrationId).toBe("public-client"); - expect(req.context!.resource!.zoneId).toBe("internal"); - callback.flowId = `flow-${++flowNumber}`; - return { - protocol: 1, - flowId: callback.flowId, - context: { - connectionId: row.id, - configRevision: req.context!.configRevision, - authorizationGeneration: req.context!.previousAuthorizationGeneration + 1n, - credentialIdentity: "ceremony", - resource: req.context!.resource, - policyDigest: "a".repeat(64), - policyRevision: "v1", - }, - authorizationUrl: "https://auth.example.test/authorize?state=secret-state", - expiresAt: BigInt(clock + 600000), - }; - }, - async completeAuthorization(req) { - if (cancelled.has(req.flowId)) throw new ConnectError("cancelled", Code.Aborted); - calls.push("complete"); - expect(req.state).toBe("secret-state"); - expect(req.code).toBe("secret-code"); - flowContext = req.context!; - current = create(CredentialStatusSchema, { - protocol: 1, - context: flowContext, - reference: { kind: CredentialKind.OAUTH, registrationId: "public-client" }, - version: (current?.version ?? 0n) + 1n, - status: "connected", - }); - if (lostCallback) throw new ConnectError("lost reply", Code.NotFound); - return current; - }, - async revoke(req) { - calls.push("revoke"); - expect(req.expected!.version).toBe(current!.version); - current = create(CredentialStatusSchema, { - ...current!, - version: current!.version + 1n, - status: "revoked", - }); - return current; - }, - }), + const control = unwrapCredentialControlClient( + createClient( + CredentialControlService, + createRouterTransport((router) => + router.service(CredentialControlService, { + async getStatus() { + if (outage) throw new ConnectError("outage", Code.Unavailable); + if (!current) throw new ConnectError("absent", Code.NotFound); + return { status: current }; + }, + async cancelAuthorization(req) { + if (outage) throw new ConnectError("outage", Code.Unavailable); + expect(req.connectionId).toBe(row.id); + cancelled.add(req.flowId); + return { + protocol: 1, + flowId: req.flowId, + connectionId: req.connectionId, + registrationId: req.registrationId, + }; + }, + async beginAuthorization(req) { + calls.push("begin"); + expect(req.sharingConsent).toBe(true); + expect(req.clientSecret).toBeUndefined(); + expect(req.registrationId).toBe("public-client"); + expect(req.context!.resource!.zoneId).toBe("internal"); + callback.flowId = `flow-${++flowNumber}`; + return { + protocol: 1, + flowId: callback.flowId, + context: { + connectionId: row.id, + configRevision: req.context!.configRevision, + authorizationGeneration: req.context!.previousAuthorizationGeneration + 1n, + credentialIdentity: "ceremony", + resource: req.context!.resource, + policyDigest: "a".repeat(64), + policyRevision: "v1", + }, + authorizationUrl: "https://auth.example.test/authorize?state=secret-state", + expiresAt: BigInt(clock + 600000), + }; + }, + async completeAuthorization(req) { + if (cancelled.has(req.flowId)) throw new ConnectError("cancelled", Code.Aborted); + calls.push("complete"); + expect(req.state).toBe("secret-state"); + expect(req.code).toBe("secret-code"); + flowContext = req.context!; + current = create(CredentialStatusSchema, { + protocol: 1, + context: flowContext, + reference: { kind: CredentialKind.OAUTH, registrationId: "public-client" }, + version: (current?.version ?? 0n) + 1n, + status: "connected", + }); + if (lostCallback) throw new ConnectError("lost reply", Code.NotFound); + return { status: current }; + }, + async revoke(req) { + calls.push("revoke"); + expect(req.expected!.version).toBe(current!.version); + current = create(CredentialStatusSchema, { + ...current!, + version: current!.version + 1n, + status: "revoked", + }); + return { status: current }; + }, + }), + ), ), ); const admin = makeCredentialAdmin({ diff --git a/orchestrator/src/__tests__/mcp-session-policy-store.test.ts b/orchestrator/src/__tests__/mcp-session-policy-store.test.ts new file mode 100644 index 000000000..5df3ce4a1 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-session-policy-store.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from "bun:test"; +import { eq } from "drizzle-orm"; +import { checkDb, getDb } from "../db/client.ts"; +import { task, taskSession, user } from "../db/schema.ts"; +import { authorizePolicyActor } from "../mcp/session-policy.ts"; +const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; +test.skipIf(!reachable)( + "policy authority uses current human owner, role, and ban state", + async () => { + const db = getDb(), + actor = crypto.randomUUID(), + tid = crypto.randomUUID(), + sid = crypto.randomUUID(); + try { + await db + .insert(user) + .values({ + id: actor, + name: "Policy owner", + email: `${actor}@example.test`, + createdAt: new Date(), + updatedAt: new Date(), + role: "user", + }); + await db.insert(task).values({ id: tid, type: "chat", createdByUserId: actor }); + await db.insert(taskSession).values({ taskId: tid, sessionId: sid }); + expect((await authorizePolicyActor(sid, actor)).isAdmin).toBe(false); + await db.update(task).set({ createdByUserId: "another-owner" }).where(eq(task.id, tid)); + await expect(authorizePolicyActor(sid, actor)).rejects.toThrow("owner or administrator"); + await db.update(user).set({ role: "admin" }).where(eq(user.id, actor)); + expect((await authorizePolicyActor(sid, actor)).isAdmin).toBe(true); + await db.update(user).set({ role: "user" }).where(eq(user.id, actor)); + await expect(authorizePolicyActor(sid, actor)).rejects.toThrow(); + await db.update(task).set({ createdByUserId: actor }).where(eq(task.id, tid)); + await db.update(user).set({ banned: true }).where(eq(user.id, actor)); + await expect(authorizePolicyActor(sid, actor)).rejects.toThrow(); + await db + .update(user) + .set({ banned: false, email: `apikey+${actor}@service.local` }) + .where(eq(user.id, actor)); + await expect(authorizePolicyActor(sid, actor)).rejects.toThrow(); + } finally { + await db.delete(task).where(eq(task.id, tid)); + await db.delete(user).where(eq(user.id, actor)); + } + }, +); diff --git a/orchestrator/src/__tests__/mcp-session-policy.test.ts b/orchestrator/src/__tests__/mcp-session-policy.test.ts new file mode 100644 index 000000000..1bd3fd43b --- /dev/null +++ b/orchestrator/src/__tests__/mcp-session-policy.test.ts @@ -0,0 +1,194 @@ +import { expect, test } from "bun:test"; +import { create } from "@bufbuild/protobuf"; +import { createHandlerContext } from "@connectrpc/connect"; +import { + SessionService, + CompleteToolCallRequestSchema, + GetMcpWritePolicyRequestSchema, + McpPlanReviewSchema, +} from "../gen/engram/app/v1/session_pb.ts"; +import { makeSessionPolicyOverrides } from "../mcp/session-policy.ts"; + +const context = () => + createHandlerContext({ + service: SessionService, + method: SessionService.method.completeToolCall, + protocolName: "connect", + requestMethod: "POST", + url: "https://test.invalid/rpc", + requestHeader: new Headers(), + responseHeader: new Headers(), + responseTrailer: new Headers(), + }); +const review = () => + create(McpPlanReviewSchema, { + policyRef: { sessionId: "session-a", policyRevision: 3n, bindingEpoch: 2n }, + toolCallId: "plan-a", + runId: "run-a", + eventIdx: 7n, + requestDigest: "a".repeat(64), + argsJson: '{"plan":"reviewed"}', + }); +test("human plan acceptance uses exact expected context and never relaxes in a preflight", async () => { + let accepted = 0; + let checked = 0; + const overrides = makeSessionPolicyOverrides({ + getSession: async () => ({ user: { id: "owner-a" } }), + authorize: async (sid, actor) => { + checked++; + expect(sid).toBe("session-a"); + expect(actor).toBe("owner-a"); + return { id: actor, isAdmin: false }; + }, + control: { + async acceptPlanExit(req) { + accepted++; + expect(req.actorId).toBe("owner-a"); + expect(req.expected).toEqual(review()); + throw new Error("unavailable"); + }, + }, + }); + const req = create(CompleteToolCallRequestSchema, { + sessionId: "session-a", + toolCallId: "plan-a", + resultJson: '{"decision":"approve"}', + expectedMcpPlanReview: review(), + }); + await expect(overrides["SessionService.CompleteToolCall"]!(req, context())).rejects.toThrow( + "unavailable", + ); + expect(accepted).toBe(1); + expect(checked).toBe(1); +}); +test("generic approval does not call the policy authority", async () => { + const overrides = makeSessionPolicyOverrides(); + expect( + await overrides["SessionService.CompleteToolCall"]!( + create(CompleteToolCallRequestSchema, { + sessionId: "s", + toolCallId: "c", + resultJson: '{"decision":"approve"}', + }), + context(), + ), + ).toBeUndefined(); +}); +test("changed identity and rejected human authorization prevent policy mutation", async () => { + let accepted = 0; + const overrides = makeSessionPolicyOverrides({ + getSession: async () => ({ user: { id: "member-a" } }), + authorize: async () => { + throw new Error("owner or administrator required"); + }, + control: { + async acceptPlanExit() { + accepted++; + return {}; + }, + }, + }); + await expect( + overrides["SessionService.CompleteToolCall"]!( + create(CompleteToolCallRequestSchema, { + sessionId: "session-a", + toolCallId: "plan-a", + resultJson: '{"decision":"approve"}', + expectedMcpPlanReview: review(), + }), + context(), + ), + ).rejects.toThrow(); + expect(accepted).toBe(0); +}); +test("policy review is read from trusted control and exposes its action", async () => { + const overrides = makeSessionPolicyOverrides({ + getSession: async () => ({ user: { id: "owner-a" } }), + authorize: async () => ({ id: "owner-a", isAdmin: false }), + control: { + async readWritePolicy() { + return { + state: { + policy: { writeEligibility: "read_only" }, + planReview: review(), + reason: "External writes are blocked.", + }, + }; + }, + }, + }); + const result = await overrides["SessionService.GetMcpWritePolicy"]!( + create(GetMcpWritePolicyRequestSchema, { sessionId: "session-a", planToolCallId: "plan-a" }), + context(), + ); + expect(result?.response).toMatchObject({ + planReview: review(), + allowedActions: ["approve_current_plan"], + }); +}); + +test("rejection keeps ordinary result delivery and drops permissive review context", async () => { + const req = create(CompleteToolCallRequestSchema, { + sessionId: "session-a", + toolCallId: "plan-a", + resultJson: '{"decision":"reject"}', + expectedMcpPlanReview: review(), + }); + expect( + await makeSessionPolicyOverrides()["SessionService.CompleteToolCall"]!(req, context()), + ).toBeUndefined(); + expect(req.expectedMcpPlanReview).toBeUndefined(); +}); + +test("public approved review replaces generic forwarding only after the trusted commit", async () => { + const { createClient, createRouterTransport } = await import("@connectrpc/connect"); + const { registerPassthrough } = await import("../rpc/passthrough.ts"); + let generic = 0, + accepted = 0; + const getSession = async () => ({ user: { id: "owner-a", role: "user" } }); + const upstream = createRouterTransport((router) => + router.service(SessionService, { + async completeToolCall(req) { + generic++; + return { sessionId: req.sessionId, note: "generic" }; + }, + }), + ); + const transport = createRouterTransport((router) => + registerPassthrough( + router, + [{ service: SessionService, methods: ["CompleteToolCall"] }], + upstream, + getSession, + async () => "owner-a", + undefined, + makeSessionPolicyOverrides({ + getSession, + authorize: async () => ({ id: "owner-a", isAdmin: false }), + control: { + async acceptPlanExit() { + accepted++; + return {}; + }, + }, + }), + ), + ); + const client = createClient(SessionService, transport); + const result = await client.completeToolCall({ + sessionId: "session-a", + toolCallId: "plan-a", + resultJson: '{"decision":"approve"}', + expectedMcpPlanReview: review(), + }); + expect(result.note).toBe("tool result queued"); + expect(accepted).toBe(1); + expect(generic).toBe(0); + await client.completeToolCall({ + sessionId: "session-a", + toolCallId: "other", + resultJson: '{"decision":"approve"}', + }); + expect(generic).toBe(1); + expect(accepted).toBe(1); +}); diff --git a/orchestrator/src/authz/policy-map.ts b/orchestrator/src/authz/policy-map.ts index e85d74856..7aadda555 100644 --- a/orchestrator/src/authz/policy-map.ts +++ b/orchestrator/src/authz/policy-map.ts @@ -54,6 +54,11 @@ export const POLICY: Record = { subject: "Session", sessionIdField: "sessionId", }, + "SessionService.GetMcpWritePolicy": { + action: "read", + subject: "Session", + sessionIdField: "sessionId", + }, "SessionService.DeleteSession": { action: "delete", subject: "Session", diff --git a/orchestrator/src/gen/engram/app/v1/session_pb.ts b/orchestrator/src/gen/engram/app/v1/session_pb.ts index 2b38adaa2..fd81b07da 100644 --- a/orchestrator/src/gen/engram/app/v1/session_pb.ts +++ b/orchestrator/src/gen/engram/app/v1/session_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/session.proto. */ export const file_engram_app_v1_session: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vYXBwL3YxL3Nlc3Npb24ucHJvdG8SDWVuZ3JhbS5hcHAudjEiFQoTTGlzdFNlc3Npb25zUmVxdWVzdCJIChRMaXN0U2Vzc2lvbnNSZXNwb25zZRIwCghzZXNzaW9ucxgBIAMoCzIeLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbkxpc3RJdGVtIrQBCg9TZXNzaW9uTGlzdEl0ZW0SJwoHc2Vzc2lvbhgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbhIYCgtvd25lcl9lbWFpbBgCIAEoCUgAiAEBEhcKCm93bmVyX25hbWUYAyABKAlIAYgBARIXCgpvd25lcl9raW5kGAQgASgJSAKIAQFCDgoMX293bmVyX2VtYWlsQg0KC19vd25lcl9uYW1lQg0KC19vd25lcl9raW5kIpgCCgdTZXNzaW9uEgoKAmlkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIUCgdob3N0X2lkGAMgASgJSACIAQESFwoKc2FuZGJveF9pZBgEIAEoCUgBiAEBEg0KBWltYWdlGAUgASgJEgwKBG1vZGUYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRIWCg5sYXN0X2FjdGl2ZV9hdBgIIAEoCRIcCg9zdWdnZXN0ZWRfdGl0bGUYCSABKAlIAogBARIaCg1sYXN0X2V2ZW50X2F0GAogASgJSAOIAQFCCgoIX2hvc3RfaWRCDQoLX3NhbmRib3hfaWRCEgoQX3N1Z2dlc3RlZF90aXRsZUIQCg5fbGFzdF9ldmVudF9hdCK4BQoUQ3JlYXRlU2Vzc2lvblJlcXVlc3QSEQoJaW1hZ2VfdXJpGAEgASgJEgwKBG1vZGUYAiABKAkSEwoGcHJvbXB0GAMgASgJSACIAQESFgoJcHJvbXB0X2lkGAcgASgJSAGIAQESQQoHc2VjcmV0cxgFIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlU2Vzc2lvblJlcXVlc3QuU2VjcmV0c0VudHJ5EkgKC2hhcm5lc3NfZW52GAYgAygLMjMuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdC5IYXJuZXNzRW52RW50cnkSFwoPc2VsZWN0ZWRfc2tpbGxzGAggAygJEhQKDGNhcGFiaWxpdGllcxgJIAMoCRIfChdpbnRlZ3JhdGlvbl9wb2xpY3lfanNvbhgKIAEoCRIUCgdoYXJuZXNzGAsgASgJSAKIAQESPwoQb2F1dGhfY3JlZGVudGlhbBgMIAEoCzIlLmVuZ3JhbS5hcHAudjEuT0F1dGhDcmVkZW50aWFsQmluZGluZxIZCgxoYXJuZXNzX21vZGUYDSABKAlIA4gBARIhChRyZXF1ZXN0ZWRfc2Vzc2lvbl9pZBgOIAEoCUgEiAEBEigKBGFwcHMYDyADKAsyGi5lbmdyYW0uYXBwLnYxLkFwcEVuZHBvaW50Gi4KDFNlY3JldHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjEKD0hhcm5lc3NFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgkKB19wcm9tcHRCDAoKX3Byb21wdF9pZEIKCghfaGFybmVzc0IPCg1faGFybmVzc19tb2RlQhcKFV9yZXF1ZXN0ZWRfc2Vzc2lvbl9pZEoECAQQBSItCgtBcHBFbmRwb2ludBIQCghob3N0bmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNIlgKFk9BdXRoQ3JlZGVudGlhbEJpbmRpbmcSLAoHc3ViamVjdBgBIAEoCzIbLmVuZ3JhbS5hcHAudjEuT0F1dGhTdWJqZWN0EhAKCHByb3ZpZGVyGAIgASgJImAKFUNyZWF0ZVNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIVCg1pbWFnZV92ZXJzaW9uGAMgASgJEgwKBGtpbmQYBCABKAkiJwoRR2V0U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSI9ChJHZXRTZXNzaW9uUmVzcG9uc2USJwoHc2Vzc2lvbhgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbiIqChREZWxldGVTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhcKFURlbGV0ZVNlc3Npb25SZXNwb25zZSJ0ChFTZW5kUHJvbXB0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEQoJcHJvbXB0X2lkGAMgASgJEhkKDGhhcm5lc3NfbW9kZRgEIAEoCUgAiAEBQg8KDV9oYXJuZXNzX21vZGUiNgoSU2VuZFByb21wdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSI2ChBJbnRlcnJ1cHRSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSDgoGc291cmNlGAIgASgJIjUKEUludGVycnVwdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSJOChdFZGl0UXVldWVkUHJvbXB0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhEKCXByb21wdF9pZBgCIAEoCRIMCgR0ZXh0GAMgASgJIjwKGEVkaXRRdWV1ZWRQcm9tcHRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEgwKBG5vdGUYAiABKAkiQwoaRGVxdWV1ZVF1ZXVlZFByb21wdFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIRCglwcm9tcHRfaWQYAiABKAkiPwobRGVxdWV1ZVF1ZXVlZFByb21wdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSJYChdDb21wbGV0ZVRvb2xDYWxsUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRITCgtyZXN1bHRfanNvbhgDIAEoCSI8ChhDb21wbGV0ZVRvb2xDYWxsUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRub3RlGAIgASgJIl0KE1N0cmVhbUV2ZW50c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRISCgVzaW5jZRgCIAEoA0gAiAEBEhQKDGR1cmFibGVfb25seRgDIAEoCEIICgZfc2luY2UiTAoMU2Vzc2lvbkV2ZW50EhAKA2lkeBgBIAEoA0gAiAEBEgwKBGtpbmQYAiABKAkSFAoMcGF5bG9hZF9qc29uGAMgASgJQgYKBF9pZHgivQEKGExpc3RTZXNzaW9uRXZlbnRzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhYKCWFmdGVyX2lkeBgCIAEoA0gAiAEBEhIKBWxpbWl0GAMgASgDSAGIAQESFwoKYmVmb3JlX2lkeBgEIAEoA0gCiAEBEg0KBWtpbmRzGAUgAygJEhIKCnRvb2xfbmFtZXMYBiADKAlCDAoKX2FmdGVyX2lkeEIICgZfbGltaXRCDQoLX2JlZm9yZV9pZHgiYAoZTGlzdFNlc3Npb25FdmVudHNSZXNwb25zZRIrCgZldmVudHMYASADKAsyGy5lbmdyYW0uYXBwLnYxLlNlc3Npb25FdmVudBIWCg5uZXh0X2FmdGVyX2lkeBgCIAEoAyKXAwoLRXhlY1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgdjb21tYW5kGAIgASgJSACIAQESDAoEYXJndhgDIAMoCRIwCgNlbnYYBCADKAsyIy5lbmdyYW0uYXBwLnYxLkV4ZWNSZXF1ZXN0LkVudkVudHJ5EhQKB3dvcmtkaXIYBSABKAlIAYgBARIZCgx0aW1lb3V0X3NlY3MYBiABKARIAogBARIUCgdleGVjX2lkGAcgASgJSAOIAQESGgoNc3Rkb3V0X29mZnNldBgIIAEoBEgEiAEBEhoKDXN0ZGVycl9vZmZzZXQYCSABKARIBYgBARIRCgR3YWtlGAogASgISAaIAQEaKgoIRW52RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfY29tbWFuZEIKCghfd29ya2RpckIPCg1fdGltZW91dF9zZWNzQgoKCF9leGVjX2lkQhAKDl9zdGRvdXRfb2Zmc2V0QhAKDl9zdGRlcnJfb2Zmc2V0QgcKBV93YWtlIjgKEUNhbmNlbEV4ZWNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSDwoHZXhlY19pZBgCIAEoCSIUChJDYW5jZWxFeGVjUmVzcG9uc2UidQoRV3JpdGVGaWxlTWV0YWRhdGESEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJEhIKCnNpemVfYnl0ZXMYAyABKAQSDgoGc2hhMjU2GAQgASgJEhEKBG1vZGUYBSABKA1IAIgBAUIHCgVfbW9kZSJiChBXcml0ZUZpbGVSZXF1ZXN0EjQKCG1ldGFkYXRhGAEgASgLMiAuZW5ncmFtLmFwcC52MS5Xcml0ZUZpbGVNZXRhZGF0YUgAEg8KBWNodW5rGAIgASgMSABCBwoFZnJhbWUiRQoRV3JpdGVGaWxlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCSIzCg9SZWFkRmlsZVJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJIlcKEFJlYWRGaWxlTWV0YWRhdGESDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCRIRCglmaWxlX25hbWUYBCABKAkiYQoQUmVhZEZpbGVSZXNwb25zZRIzCghtZXRhZGF0YRgBIAEoCzIfLmVuZ3JhbS5hcHAudjEuUmVhZEZpbGVNZXRhZGF0YUgAEg8KBWNodW5rGAIgASgMSABCBwoFZnJhbWUiVwoQQ29weUZpbGVzUmVxdWVzdBIZChFzb3VyY2Vfc2Vzc2lvbl9pZBgBIAEoCRIZChF0YXJnZXRfc2Vzc2lvbl9pZBgCIAEoCRINCgVwYXRocxgDIAMoCSJCCg5Db3B5RmlsZVJlc3VsdBIMCgRwYXRoGAEgASgJEhIKCnNpemVfYnl0ZXMYAiABKAQSDgoGc2hhMjU2GAMgASgJIkEKEUNvcHlGaWxlc1Jlc3BvbnNlEiwKBWZpbGVzGAEgAygLMh0uZW5ncmFtLmFwcC52MS5Db3B5RmlsZVJlc3VsdCKRAQoKRXhlY091dHB1dBItCgdzdGFydGVkGAEgASgLMhouZW5ncmFtLmFwcC52MS5FeGVjU3RhcnRlZEgAEhAKBnN0ZG91dBgCIAEoDEgAEhAKBnN0ZGVychgDIAEoDEgAEicKBGV4aXQYBCABKAsyFy5lbmdyYW0uYXBwLnYxLkV4ZWNFeGl0SABCBwoFZXZlbnQiHgoLRXhlY1N0YXJ0ZWQSDwoHZXhlY19pZBgBIAEoCSJfCghFeGVjRXhpdBIYCgtleGl0X3N0YXR1cxgBIAEoBUgAiAEBEikKBnJ1c2FnZRgCIAEoCzIZLmVuZ3JhbS5hcHAudjEuRXhlY1J1c2FnZUIOCgxfZXhpdF9zdGF0dXMimQEKCkV4ZWNSdXNhZ2USDwoHd2FsbF9tcxgBIAEoBBIYCgtwZWFrX3Jzc19rYhgCIAEoBEgAiAEBEhgKC3VzZXJfY3B1X21zGAMgASgESAGIAQESFwoKc3lzX2NwdV9tcxgEIAEoBEgCiAEBQg4KDF9wZWFrX3Jzc19rYkIOCgxfdXNlcl9jcHVfbXNCDQoLX3N5c19jcHVfbXMieQoNR2V0TG9nUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhEKBGtpbmQYAiABKAlIAIgBARISCgVsaW1pdBgDIAEoA0gBiAEBEhEKBHRhaWwYBCABKAhIAogBAUIHCgVfa2luZEIICgZfbGltaXRCBwoFX3RhaWwiZAoOR2V0TG9nUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRraW5kGAIgASgJEjAKBmV2ZW50cxgDIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ29udmVyc2F0aW9uRW50cnkiUAoRQ29udmVyc2F0aW9uRW50cnkSCwoDaWR4GAEgASgDEgwKBGtpbmQYAiABKAkSCgoCYXQYAyABKAkSFAoMcGF5bG9hZF9qc29uGAQgASgJIiUKD1NuYXBzaG90UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIoYBChBTbmFwc2hvdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSGAoLc25hcHNob3RfaWQYAiABKAlIAIgBARIXCgpzaXplX2J5dGVzGAMgASgESAGIAQESDAoEbm90ZRgEIAEoCUIOCgxfc25hcHNob3RfaWRCDQoLX3NpemVfYnl0ZXMiKgoURW5zdXJlQnJvd3NlclJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIlChVFbnN1cmVCcm93c2VyUmVzcG9uc2USDAoEcG9ydBgBIAEoDSImChBFbnN1cmVJZGVSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiIQoRRW5zdXJlSWRlUmVzcG9uc2USDAoEcG9ydBgBIAEoDSIjCg1SZXN1bWVSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkihAEKDlJlc3VtZVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSGAoLc25hcHNob3RfaWQYAiABKAlIAIgBARIXCgpzaXplX2J5dGVzGAMgASgESAGIAQESDAoEbm90ZRgEIAEoCUIOCgxfc25hcHNob3RfaWRCDQoLX3NpemVfYnl0ZXMiJwoRRXZpY3RMb2NhbFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIUChJFdmljdExvY2FsUmVzcG9uc2UiJgoQRXZpY3RJZGxlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjcKEUV2aWN0SWRsZVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDgoGc3RhdHVzGAIgASgJIigKEkdldENvd1N0YXRlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJImQKE0dldENvd1N0YXRlUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIvCgVzdGF0ZRgCIAEoCzIbLmVuZ3JhbS5hcHAudjEuQ293U3RhdGVWaWV3SACIAQFCCAoGX3N0YXRlIroDCgxDb3dTdGF0ZVZpZXcSEgoKc2FuZGJveF9pZBgBIAEoCRIXCgpzZXNzaW9uX2lkGAIgASgJSACIAQESGAoQZGlza19tYW5pZmVzdF9pZBgDIAEoCRIdChVkaXNrX21hbmlmZXN0X3ZlcnNpb24YBCABKAQSFAoMZGlydHlfY2h1bmtzGAUgASgNEhMKC2RpcnR5X2J5dGVzGAYgASgEEhoKDWxhc3RfZmx1c2hfYXQYByABKAlIAYgBARITCgtiYXNlX2NodW5rcxgIIAEoDRIZChFiYXNlX2NodW5rc19sb2NhbBgJIAEoDRIfChJtZW1vcnlfbWFuaWZlc3RfaWQYCiABKAlIAogBARIkChdtZW1vcnlfbWFuaWZlc3RfdmVyc2lvbhgLIAEoBEgDiAEBEh0KEGxhc3Rfc25hcHNob3RfYXQYDCABKAlIBIgBAUINCgtfc2Vzc2lvbl9pZEIQCg5fbGFzdF9mbHVzaF9hdEIVChNfbWVtb3J5X21hbmlmZXN0X2lkQhoKGF9tZW1vcnlfbWFuaWZlc3RfdmVyc2lvbkITChFfbGFzdF9zbmFwc2hvdF9hdCIsChZMaXN0Q2hlY2twb2ludHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiZAoXTGlzdENoZWNrcG9pbnRzUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRI1CgtjaGVja3BvaW50cxgCIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ2hlY2twb2ludFN1bW1hcnkipgEKEUNoZWNrcG9pbnRTdW1tYXJ5EhMKC3NuYXBzaG90X2lkGAEgASgJEhIKCmNyZWF0ZWRfYXQYAiABKAkSEgoKc2l6ZV9ieXRlcxgDIAEoBBIaCg1ldmVudHNfY3Vyc29yGAQgASgDSACIAQESEwoLcmVjb3ZlcmFibGUYBSABKAgSEQoJaXNfbGF0ZXN0GAYgASgIQhAKDl9ldmVudHNfY3Vyc29yIj0KEkdldEFydGlmYWN0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhMKC2FydGlmYWN0X2lkGAIgASgJImIKE0dldEFydGlmYWN0UmVzcG9uc2USMwoIbWV0YWRhdGEYASABKAsyHy5lbmdyYW0uYXBwLnYxLkFydGlmYWN0TWV0YWRhdGFIABIPCgVjaHVuaxgCIAEoDEgAQgUKA21zZyJNChBBcnRpZmFjdE1ldGFkYXRhEhIKCm1lZGlhX3R5cGUYASABKAkSEgoKc2l6ZV9ieXRlcxgCIAEoBBIRCglmaWxlX25hbWUYAyABKAkiewodQ3JlYXRlQXJ0aWZhY3RGcm9tUGF0aFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJEhQKB2NhcHRpb24YAyABKAlIAIgBARIWCg5zdXBwcmVzc19ldmVudBgEIAEoCEIKCghfY2FwdGlvbiJdCh5DcmVhdGVBcnRpZmFjdEZyb21QYXRoUmVzcG9uc2USEwoLYXJ0aWZhY3RfaWQYASABKAkSEgoKbWVkaWFfdHlwZRgCIAEoCRISCgpzaXplX2J5dGVzGAMgASgEIrQBChFSZWxheVNoZWxsUmVxdWVzdBIoCgRvcGVuGAEgASgLMhguZW5ncmFtLmFwcC52MS5TaGVsbE9wZW5IABIOCgR0ZXh0GAIgASgJSAASEAoGYmluYXJ5GAMgASgMSAASDgoEcGluZxgEIAEoDEgAEg4KBHBvbmcYBSABKAxIABIqCgVjbG9zZRgGIAEoCzIZLmVuZ3JhbS5hcHAudjEuU2hlbGxDbG9zZUgAQgcKBWZyYW1lIosBChJSZWxheVNoZWxsUmVzcG9uc2USDgoEdGV4dBgBIAEoCUgAEhAKBmJpbmFyeRgCIAEoDEgAEg4KBHBpbmcYAyABKAxIABIOCgRwb25nGAQgASgMSAASKgoFY2xvc2UYBSABKAsyGS5lbmdyYW0uYXBwLnYxLlNoZWxsQ2xvc2VIAEIHCgVmcmFtZSIfCglTaGVsbE9wZW4SEgoKc2Vzc2lvbl9pZBgBIAEoCSIqCgpTaGVsbENsb3NlEgwKBGNvZGUYASABKA0SDgoGcmVhc29uGAIgASgJIn8KEFJlbGF5UG9ydFJlcXVlc3QSJwoEb3BlbhgBIAEoCzIXLmVuZ3JhbS5hcHAudjEuUG9ydE9wZW5IABIOCgRkYXRhGAIgASgMSAASKQoFY2xvc2UYAyABKAsyGC5lbmdyYW0uYXBwLnYxLlBvcnRDbG9zZUgAQgcKBWZyYW1lIlcKEVJlbGF5UG9ydFJlc3BvbnNlEg4KBGRhdGEYASABKAxIABIpCgVjbG9zZRgCIAEoCzIYLmVuZ3JhbS5hcHAudjEuUG9ydENsb3NlSABCBwoFZnJhbWUiLAoIUG9ydE9wZW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwb3J0GAIgASgNIgsKCVBvcnRDbG9zZTLAEgoOU2Vzc2lvblNlcnZpY2USVwoMTGlzdFNlc3Npb25zEiIuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbnNSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbnNSZXNwb25zZRJaCg1DcmVhdGVTZXNzaW9uEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlU2Vzc2lvblJlc3BvbnNlElEKCkdldFNlc3Npb24SIC5lbmdyYW0uYXBwLnYxLkdldFNlc3Npb25SZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uUmVzcG9uc2USWgoNRGVsZXRlU2Vzc2lvbhIjLmVuZ3JhbS5hcHAudjEuRGVsZXRlU2Vzc2lvblJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkRlbGV0ZVNlc3Npb25SZXNwb25zZRJRCgpTZW5kUHJvbXB0EiAuZW5ncmFtLmFwcC52MS5TZW5kUHJvbXB0UmVxdWVzdBohLmVuZ3JhbS5hcHAudjEuU2VuZFByb21wdFJlc3BvbnNlEk4KCUludGVycnVwdBIfLmVuZ3JhbS5hcHAudjEuSW50ZXJydXB0UmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuSW50ZXJydXB0UmVzcG9uc2USYwoQRWRpdFF1ZXVlZFByb21wdBImLmVuZ3JhbS5hcHAudjEuRWRpdFF1ZXVlZFByb21wdFJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkVkaXRRdWV1ZWRQcm9tcHRSZXNwb25zZRJsChNEZXF1ZXVlUXVldWVkUHJvbXB0EikuZW5ncmFtLmFwcC52MS5EZXF1ZXVlUXVldWVkUHJvbXB0UmVxdWVzdBoqLmVuZ3JhbS5hcHAudjEuRGVxdWV1ZVF1ZXVlZFByb21wdFJlc3BvbnNlEmMKEENvbXBsZXRlVG9vbENhbGwSJi5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlVG9vbENhbGxSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5Db21wbGV0ZVRvb2xDYWxsUmVzcG9uc2USUQoMU3RyZWFtRXZlbnRzEiIuZW5ncmFtLmFwcC52MS5TdHJlYW1FdmVudHNSZXF1ZXN0GhsuZW5ncmFtLmFwcC52MS5TZXNzaW9uRXZlbnQwARJmChFMaXN0U2Vzc2lvbkV2ZW50cxInLmVuZ3JhbS5hcHAudjEuTGlzdFNlc3Npb25FdmVudHNSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbkV2ZW50c1Jlc3BvbnNlEj8KBEV4ZWMSGi5lbmdyYW0uYXBwLnYxLkV4ZWNSZXF1ZXN0GhkuZW5ncmFtLmFwcC52MS5FeGVjT3V0cHV0MAESUQoKQ2FuY2VsRXhlYxIgLmVuZ3JhbS5hcHAudjEuQ2FuY2VsRXhlY1JlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkNhbmNlbEV4ZWNSZXNwb25zZRJQCglXcml0ZUZpbGUSHy5lbmdyYW0uYXBwLnYxLldyaXRlRmlsZVJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLldyaXRlRmlsZVJlc3BvbnNlKAESTQoIUmVhZEZpbGUSHi5lbmdyYW0uYXBwLnYxLlJlYWRGaWxlUmVxdWVzdBofLmVuZ3JhbS5hcHAudjEuUmVhZEZpbGVSZXNwb25zZTABEk4KCUNvcHlGaWxlcxIfLmVuZ3JhbS5hcHAudjEuQ29weUZpbGVzUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuQ29weUZpbGVzUmVzcG9uc2USRQoGR2V0TG9nEhwuZW5ncmFtLmFwcC52MS5HZXRMb2dSZXF1ZXN0Gh0uZW5ncmFtLmFwcC52MS5HZXRMb2dSZXNwb25zZRJLCghTbmFwc2hvdBIeLmVuZ3JhbS5hcHAudjEuU25hcHNob3RSZXF1ZXN0Gh8uZW5ncmFtLmFwcC52MS5TbmFwc2hvdFJlc3BvbnNlEloKDUVuc3VyZUJyb3dzZXISIy5lbmdyYW0uYXBwLnYxLkVuc3VyZUJyb3dzZXJSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5FbnN1cmVCcm93c2VyUmVzcG9uc2USTgoJRW5zdXJlSWRlEh8uZW5ncmFtLmFwcC52MS5FbnN1cmVJZGVSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5FbnN1cmVJZGVSZXNwb25zZRJFCgZSZXN1bWUSHC5lbmdyYW0uYXBwLnYxLlJlc3VtZVJlcXVlc3QaHS5lbmdyYW0uYXBwLnYxLlJlc3VtZVJlc3BvbnNlElEKCkV2aWN0TG9jYWwSIC5lbmdyYW0uYXBwLnYxLkV2aWN0TG9jYWxSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5FdmljdExvY2FsUmVzcG9uc2USTgoJRXZpY3RJZGxlEh8uZW5ncmFtLmFwcC52MS5FdmljdElkbGVSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5FdmljdElkbGVSZXNwb25zZRJUCgtHZXRDb3dTdGF0ZRIhLmVuZ3JhbS5hcHAudjEuR2V0Q293U3RhdGVSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRDb3dTdGF0ZVJlc3BvbnNlEmAKD0xpc3RDaGVja3BvaW50cxIlLmVuZ3JhbS5hcHAudjEuTGlzdENoZWNrcG9pbnRzUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuTGlzdENoZWNrcG9pbnRzUmVzcG9uc2USVgoLR2V0QXJ0aWZhY3QSIS5lbmdyYW0uYXBwLnYxLkdldEFydGlmYWN0UmVxdWVzdBoiLmVuZ3JhbS5hcHAudjEuR2V0QXJ0aWZhY3RSZXNwb25zZTABEnUKFkNyZWF0ZUFydGlmYWN0RnJvbVBhdGgSLC5lbmdyYW0uYXBwLnYxLkNyZWF0ZUFydGlmYWN0RnJvbVBhdGhSZXF1ZXN0Gi0uZW5ncmFtLmFwcC52MS5DcmVhdGVBcnRpZmFjdEZyb21QYXRoUmVzcG9uc2UyZQoRU2hlbGxSZWxheVNlcnZpY2USUAoFUmVsYXkSIC5lbmdyYW0uYXBwLnYxLlJlbGF5U2hlbGxSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5SZWxheVNoZWxsUmVzcG9uc2UoATABMmIKEFBvcnRSZWxheVNlcnZpY2USTgoFUmVsYXkSHy5lbmdyYW0uYXBwLnYxLlJlbGF5UG9ydFJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLlJlbGF5UG9ydFJlc3BvbnNlKAEwAWIGcHJvdG8z", [file_engram_app_v1_oauth]); + fileDesc("ChtlbmdyYW0vYXBwL3YxL3Nlc3Npb24ucHJvdG8SDWVuZ3JhbS5hcHAudjEiFQoTTGlzdFNlc3Npb25zUmVxdWVzdCJIChRMaXN0U2Vzc2lvbnNSZXNwb25zZRIwCghzZXNzaW9ucxgBIAMoCzIeLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbkxpc3RJdGVtIrQBCg9TZXNzaW9uTGlzdEl0ZW0SJwoHc2Vzc2lvbhgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbhIYCgtvd25lcl9lbWFpbBgCIAEoCUgAiAEBEhcKCm93bmVyX25hbWUYAyABKAlIAYgBARIXCgpvd25lcl9raW5kGAQgASgJSAKIAQFCDgoMX293bmVyX2VtYWlsQg0KC19vd25lcl9uYW1lQg0KC19vd25lcl9raW5kIpgCCgdTZXNzaW9uEgoKAmlkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIUCgdob3N0X2lkGAMgASgJSACIAQESFwoKc2FuZGJveF9pZBgEIAEoCUgBiAEBEg0KBWltYWdlGAUgASgJEgwKBG1vZGUYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRIWCg5sYXN0X2FjdGl2ZV9hdBgIIAEoCRIcCg9zdWdnZXN0ZWRfdGl0bGUYCSABKAlIAogBARIaCg1sYXN0X2V2ZW50X2F0GAogASgJSAOIAQFCCgoIX2hvc3RfaWRCDQoLX3NhbmRib3hfaWRCEgoQX3N1Z2dlc3RlZF90aXRsZUIQCg5fbGFzdF9ldmVudF9hdCK4BQoUQ3JlYXRlU2Vzc2lvblJlcXVlc3QSEQoJaW1hZ2VfdXJpGAEgASgJEgwKBG1vZGUYAiABKAkSEwoGcHJvbXB0GAMgASgJSACIAQESFgoJcHJvbXB0X2lkGAcgASgJSAGIAQESQQoHc2VjcmV0cxgFIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlU2Vzc2lvblJlcXVlc3QuU2VjcmV0c0VudHJ5EkgKC2hhcm5lc3NfZW52GAYgAygLMjMuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdC5IYXJuZXNzRW52RW50cnkSFwoPc2VsZWN0ZWRfc2tpbGxzGAggAygJEhQKDGNhcGFiaWxpdGllcxgJIAMoCRIfChdpbnRlZ3JhdGlvbl9wb2xpY3lfanNvbhgKIAEoCRIUCgdoYXJuZXNzGAsgASgJSAKIAQESPwoQb2F1dGhfY3JlZGVudGlhbBgMIAEoCzIlLmVuZ3JhbS5hcHAudjEuT0F1dGhDcmVkZW50aWFsQmluZGluZxIZCgxoYXJuZXNzX21vZGUYDSABKAlIA4gBARIhChRyZXF1ZXN0ZWRfc2Vzc2lvbl9pZBgOIAEoCUgEiAEBEigKBGFwcHMYDyADKAsyGi5lbmdyYW0uYXBwLnYxLkFwcEVuZHBvaW50Gi4KDFNlY3JldHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjEKD0hhcm5lc3NFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgkKB19wcm9tcHRCDAoKX3Byb21wdF9pZEIKCghfaGFybmVzc0IPCg1faGFybmVzc19tb2RlQhcKFV9yZXF1ZXN0ZWRfc2Vzc2lvbl9pZEoECAQQBSItCgtBcHBFbmRwb2ludBIQCghob3N0bmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNIlgKFk9BdXRoQ3JlZGVudGlhbEJpbmRpbmcSLAoHc3ViamVjdBgBIAEoCzIbLmVuZ3JhbS5hcHAudjEuT0F1dGhTdWJqZWN0EhAKCHByb3ZpZGVyGAIgASgJImAKFUNyZWF0ZVNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIVCg1pbWFnZV92ZXJzaW9uGAMgASgJEgwKBGtpbmQYBCABKAkiJwoRR2V0U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSI9ChJHZXRTZXNzaW9uUmVzcG9uc2USJwoHc2Vzc2lvbhgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbiIqChREZWxldGVTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhcKFURlbGV0ZVNlc3Npb25SZXNwb25zZSJ0ChFTZW5kUHJvbXB0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEQoJcHJvbXB0X2lkGAMgASgJEhkKDGhhcm5lc3NfbW9kZRgEIAEoCUgAiAEBQg8KDV9oYXJuZXNzX21vZGUiNgoSU2VuZFByb21wdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSI2ChBJbnRlcnJ1cHRSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSDgoGc291cmNlGAIgASgJIjUKEUludGVycnVwdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSJOChdFZGl0UXVldWVkUHJvbXB0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhEKCXByb21wdF9pZBgCIAEoCRIMCgR0ZXh0GAMgASgJIjwKGEVkaXRRdWV1ZWRQcm9tcHRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEgwKBG5vdGUYAiABKAkiQwoaRGVxdWV1ZVF1ZXVlZFByb21wdFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIRCglwcm9tcHRfaWQYAiABKAkiPwobRGVxdWV1ZVF1ZXVlZFByb21wdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSJXChFNY3BXcml0ZVBvbGljeVJlZhISCgpzZXNzaW9uX2lkGAEgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgCIAEoAxIVCg1iaW5kaW5nX2Vwb2NoGAMgASgDIscDChVNY3BUcnVzdGVkV3JpdGVQb2xpY3kSLQoDcmVmGAEgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIZChF3cml0ZV9lbGlnaWJpbGl0eRgCIAEoCRINCgVjYXVzZRgDIAEoCRIZChFhY2NlcHRlZF9pbnB1dF9pZBgEIAEoCRIjChZhdXRoZW50aWNhdGVkX2FjdG9yX2lkGAUgASgJSACIAQESFgoJcHJvbXB0X2lkGAYgASgJSAGIAQESHgoRcGxhbl90b29sX2NhbGxfaWQYByABKAlIAogBARIgChNwbGFuX3JlcXVlc3RfZGlnZXN0GAggASgJSAOIAQESPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYCSADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEg8KB2N1cnJlbnQYCiABKAgSEwoLaW5pdGlhbGl6ZWQYCyABKAhCGQoXX2F1dGhlbnRpY2F0ZWRfYWN0b3JfaWRCDAoKX3Byb21wdF9pZEIUChJfcGxhbl90b29sX2NhbGxfaWRCFgoUX3BsYW5fcmVxdWVzdF9kaWdlc3QiqQEKDU1jcFBsYW5SZXZpZXcSNAoKcG9saWN5X3JlZhgBIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFAoMdG9vbF9jYWxsX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIRCglldmVudF9pZHgYBCABKAMSFgoOcmVxdWVzdF9kaWdlc3QYBSABKAkSEQoJYXJnc19qc29uGAYgASgJImQKGEdldE1jcFdyaXRlUG9saWN5UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh4KEXBsYW5fdG9vbF9jYWxsX2lkGAIgASgJSACIAQFCFAoSX3BsYW5fdG9vbF9jYWxsX2lkIq0BChlHZXRNY3BXcml0ZVBvbGljeVJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5EjEKC3BsYW5fcmV2aWV3GAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3Eg4KBnJlYXNvbhgDIAEoCRIXCg9hbGxvd2VkX2FjdGlvbnMYBCADKAkimAEKF0NvbXBsZXRlVG9vbENhbGxSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJEhMKC3Jlc3VsdF9qc29uGAMgASgJEj4KGGV4cGVjdGVkX21jcF9wbGFuX3JldmlldxgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuTWNwUGxhblJldmlldyI8ChhDb21wbGV0ZVRvb2xDYWxsUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRub3RlGAIgASgJIl0KE1N0cmVhbUV2ZW50c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRISCgVzaW5jZRgCIAEoA0gAiAEBEhQKDGR1cmFibGVfb25seRgDIAEoCEIICgZfc2luY2UiTAoMU2Vzc2lvbkV2ZW50EhAKA2lkeBgBIAEoA0gAiAEBEgwKBGtpbmQYAiABKAkSFAoMcGF5bG9hZF9qc29uGAMgASgJQgYKBF9pZHgivQEKGExpc3RTZXNzaW9uRXZlbnRzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhYKCWFmdGVyX2lkeBgCIAEoA0gAiAEBEhIKBWxpbWl0GAMgASgDSAGIAQESFwoKYmVmb3JlX2lkeBgEIAEoA0gCiAEBEg0KBWtpbmRzGAUgAygJEhIKCnRvb2xfbmFtZXMYBiADKAlCDAoKX2FmdGVyX2lkeEIICgZfbGltaXRCDQoLX2JlZm9yZV9pZHgiYAoZTGlzdFNlc3Npb25FdmVudHNSZXNwb25zZRIrCgZldmVudHMYASADKAsyGy5lbmdyYW0uYXBwLnYxLlNlc3Npb25FdmVudBIWCg5uZXh0X2FmdGVyX2lkeBgCIAEoAyKXAwoLRXhlY1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgdjb21tYW5kGAIgASgJSACIAQESDAoEYXJndhgDIAMoCRIwCgNlbnYYBCADKAsyIy5lbmdyYW0uYXBwLnYxLkV4ZWNSZXF1ZXN0LkVudkVudHJ5EhQKB3dvcmtkaXIYBSABKAlIAYgBARIZCgx0aW1lb3V0X3NlY3MYBiABKARIAogBARIUCgdleGVjX2lkGAcgASgJSAOIAQESGgoNc3Rkb3V0X29mZnNldBgIIAEoBEgEiAEBEhoKDXN0ZGVycl9vZmZzZXQYCSABKARIBYgBARIRCgR3YWtlGAogASgISAaIAQEaKgoIRW52RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfY29tbWFuZEIKCghfd29ya2RpckIPCg1fdGltZW91dF9zZWNzQgoKCF9leGVjX2lkQhAKDl9zdGRvdXRfb2Zmc2V0QhAKDl9zdGRlcnJfb2Zmc2V0QgcKBV93YWtlIjgKEUNhbmNlbEV4ZWNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSDwoHZXhlY19pZBgCIAEoCSIUChJDYW5jZWxFeGVjUmVzcG9uc2UidQoRV3JpdGVGaWxlTWV0YWRhdGESEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJEhIKCnNpemVfYnl0ZXMYAyABKAQSDgoGc2hhMjU2GAQgASgJEhEKBG1vZGUYBSABKA1IAIgBAUIHCgVfbW9kZSJiChBXcml0ZUZpbGVSZXF1ZXN0EjQKCG1ldGFkYXRhGAEgASgLMiAuZW5ncmFtLmFwcC52MS5Xcml0ZUZpbGVNZXRhZGF0YUgAEg8KBWNodW5rGAIgASgMSABCBwoFZnJhbWUiRQoRV3JpdGVGaWxlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCSIzCg9SZWFkRmlsZVJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJIlcKEFJlYWRGaWxlTWV0YWRhdGESDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCRIRCglmaWxlX25hbWUYBCABKAkiYQoQUmVhZEZpbGVSZXNwb25zZRIzCghtZXRhZGF0YRgBIAEoCzIfLmVuZ3JhbS5hcHAudjEuUmVhZEZpbGVNZXRhZGF0YUgAEg8KBWNodW5rGAIgASgMSABCBwoFZnJhbWUiVwoQQ29weUZpbGVzUmVxdWVzdBIZChFzb3VyY2Vfc2Vzc2lvbl9pZBgBIAEoCRIZChF0YXJnZXRfc2Vzc2lvbl9pZBgCIAEoCRINCgVwYXRocxgDIAMoCSJCCg5Db3B5RmlsZVJlc3VsdBIMCgRwYXRoGAEgASgJEhIKCnNpemVfYnl0ZXMYAiABKAQSDgoGc2hhMjU2GAMgASgJIkEKEUNvcHlGaWxlc1Jlc3BvbnNlEiwKBWZpbGVzGAEgAygLMh0uZW5ncmFtLmFwcC52MS5Db3B5RmlsZVJlc3VsdCKRAQoKRXhlY091dHB1dBItCgdzdGFydGVkGAEgASgLMhouZW5ncmFtLmFwcC52MS5FeGVjU3RhcnRlZEgAEhAKBnN0ZG91dBgCIAEoDEgAEhAKBnN0ZGVychgDIAEoDEgAEicKBGV4aXQYBCABKAsyFy5lbmdyYW0uYXBwLnYxLkV4ZWNFeGl0SABCBwoFZXZlbnQiHgoLRXhlY1N0YXJ0ZWQSDwoHZXhlY19pZBgBIAEoCSJfCghFeGVjRXhpdBIYCgtleGl0X3N0YXR1cxgBIAEoBUgAiAEBEikKBnJ1c2FnZRgCIAEoCzIZLmVuZ3JhbS5hcHAudjEuRXhlY1J1c2FnZUIOCgxfZXhpdF9zdGF0dXMimQEKCkV4ZWNSdXNhZ2USDwoHd2FsbF9tcxgBIAEoBBIYCgtwZWFrX3Jzc19rYhgCIAEoBEgAiAEBEhgKC3VzZXJfY3B1X21zGAMgASgESAGIAQESFwoKc3lzX2NwdV9tcxgEIAEoBEgCiAEBQg4KDF9wZWFrX3Jzc19rYkIOCgxfdXNlcl9jcHVfbXNCDQoLX3N5c19jcHVfbXMieQoNR2V0TG9nUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhEKBGtpbmQYAiABKAlIAIgBARISCgVsaW1pdBgDIAEoA0gBiAEBEhEKBHRhaWwYBCABKAhIAogBAUIHCgVfa2luZEIICgZfbGltaXRCBwoFX3RhaWwiZAoOR2V0TG9nUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRraW5kGAIgASgJEjAKBmV2ZW50cxgDIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ29udmVyc2F0aW9uRW50cnkiUAoRQ29udmVyc2F0aW9uRW50cnkSCwoDaWR4GAEgASgDEgwKBGtpbmQYAiABKAkSCgoCYXQYAyABKAkSFAoMcGF5bG9hZF9qc29uGAQgASgJIiUKD1NuYXBzaG90UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIoYBChBTbmFwc2hvdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSGAoLc25hcHNob3RfaWQYAiABKAlIAIgBARIXCgpzaXplX2J5dGVzGAMgASgESAGIAQESDAoEbm90ZRgEIAEoCUIOCgxfc25hcHNob3RfaWRCDQoLX3NpemVfYnl0ZXMiKgoURW5zdXJlQnJvd3NlclJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIlChVFbnN1cmVCcm93c2VyUmVzcG9uc2USDAoEcG9ydBgBIAEoDSImChBFbnN1cmVJZGVSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiIQoRRW5zdXJlSWRlUmVzcG9uc2USDAoEcG9ydBgBIAEoDSIjCg1SZXN1bWVSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkihAEKDlJlc3VtZVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSGAoLc25hcHNob3RfaWQYAiABKAlIAIgBARIXCgpzaXplX2J5dGVzGAMgASgESAGIAQESDAoEbm90ZRgEIAEoCUIOCgxfc25hcHNob3RfaWRCDQoLX3NpemVfYnl0ZXMiJwoRRXZpY3RMb2NhbFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIUChJFdmljdExvY2FsUmVzcG9uc2UiJgoQRXZpY3RJZGxlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjcKEUV2aWN0SWRsZVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDgoGc3RhdHVzGAIgASgJIigKEkdldENvd1N0YXRlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJImQKE0dldENvd1N0YXRlUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIvCgVzdGF0ZRgCIAEoCzIbLmVuZ3JhbS5hcHAudjEuQ293U3RhdGVWaWV3SACIAQFCCAoGX3N0YXRlIroDCgxDb3dTdGF0ZVZpZXcSEgoKc2FuZGJveF9pZBgBIAEoCRIXCgpzZXNzaW9uX2lkGAIgASgJSACIAQESGAoQZGlza19tYW5pZmVzdF9pZBgDIAEoCRIdChVkaXNrX21hbmlmZXN0X3ZlcnNpb24YBCABKAQSFAoMZGlydHlfY2h1bmtzGAUgASgNEhMKC2RpcnR5X2J5dGVzGAYgASgEEhoKDWxhc3RfZmx1c2hfYXQYByABKAlIAYgBARITCgtiYXNlX2NodW5rcxgIIAEoDRIZChFiYXNlX2NodW5rc19sb2NhbBgJIAEoDRIfChJtZW1vcnlfbWFuaWZlc3RfaWQYCiABKAlIAogBARIkChdtZW1vcnlfbWFuaWZlc3RfdmVyc2lvbhgLIAEoBEgDiAEBEh0KEGxhc3Rfc25hcHNob3RfYXQYDCABKAlIBIgBAUINCgtfc2Vzc2lvbl9pZEIQCg5fbGFzdF9mbHVzaF9hdEIVChNfbWVtb3J5X21hbmlmZXN0X2lkQhoKGF9tZW1vcnlfbWFuaWZlc3RfdmVyc2lvbkITChFfbGFzdF9zbmFwc2hvdF9hdCIsChZMaXN0Q2hlY2twb2ludHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiZAoXTGlzdENoZWNrcG9pbnRzUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRI1CgtjaGVja3BvaW50cxgCIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ2hlY2twb2ludFN1bW1hcnkipgEKEUNoZWNrcG9pbnRTdW1tYXJ5EhMKC3NuYXBzaG90X2lkGAEgASgJEhIKCmNyZWF0ZWRfYXQYAiABKAkSEgoKc2l6ZV9ieXRlcxgDIAEoBBIaCg1ldmVudHNfY3Vyc29yGAQgASgDSACIAQESEwoLcmVjb3ZlcmFibGUYBSABKAgSEQoJaXNfbGF0ZXN0GAYgASgIQhAKDl9ldmVudHNfY3Vyc29yIj0KEkdldEFydGlmYWN0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhMKC2FydGlmYWN0X2lkGAIgASgJImIKE0dldEFydGlmYWN0UmVzcG9uc2USMwoIbWV0YWRhdGEYASABKAsyHy5lbmdyYW0uYXBwLnYxLkFydGlmYWN0TWV0YWRhdGFIABIPCgVjaHVuaxgCIAEoDEgAQgUKA21zZyJNChBBcnRpZmFjdE1ldGFkYXRhEhIKCm1lZGlhX3R5cGUYASABKAkSEgoKc2l6ZV9ieXRlcxgCIAEoBBIRCglmaWxlX25hbWUYAyABKAkiewodQ3JlYXRlQXJ0aWZhY3RGcm9tUGF0aFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJEhQKB2NhcHRpb24YAyABKAlIAIgBARIWCg5zdXBwcmVzc19ldmVudBgEIAEoCEIKCghfY2FwdGlvbiJdCh5DcmVhdGVBcnRpZmFjdEZyb21QYXRoUmVzcG9uc2USEwoLYXJ0aWZhY3RfaWQYASABKAkSEgoKbWVkaWFfdHlwZRgCIAEoCRISCgpzaXplX2J5dGVzGAMgASgEIrQBChFSZWxheVNoZWxsUmVxdWVzdBIoCgRvcGVuGAEgASgLMhguZW5ncmFtLmFwcC52MS5TaGVsbE9wZW5IABIOCgR0ZXh0GAIgASgJSAASEAoGYmluYXJ5GAMgASgMSAASDgoEcGluZxgEIAEoDEgAEg4KBHBvbmcYBSABKAxIABIqCgVjbG9zZRgGIAEoCzIZLmVuZ3JhbS5hcHAudjEuU2hlbGxDbG9zZUgAQgcKBWZyYW1lIosBChJSZWxheVNoZWxsUmVzcG9uc2USDgoEdGV4dBgBIAEoCUgAEhAKBmJpbmFyeRgCIAEoDEgAEg4KBHBpbmcYAyABKAxIABIOCgRwb25nGAQgASgMSAASKgoFY2xvc2UYBSABKAsyGS5lbmdyYW0uYXBwLnYxLlNoZWxsQ2xvc2VIAEIHCgVmcmFtZSIfCglTaGVsbE9wZW4SEgoKc2Vzc2lvbl9pZBgBIAEoCSIqCgpTaGVsbENsb3NlEgwKBGNvZGUYASABKA0SDgoGcmVhc29uGAIgASgJIn8KEFJlbGF5UG9ydFJlcXVlc3QSJwoEb3BlbhgBIAEoCzIXLmVuZ3JhbS5hcHAudjEuUG9ydE9wZW5IABIOCgRkYXRhGAIgASgMSAASKQoFY2xvc2UYAyABKAsyGC5lbmdyYW0uYXBwLnYxLlBvcnRDbG9zZUgAQgcKBWZyYW1lIlcKEVJlbGF5UG9ydFJlc3BvbnNlEg4KBGRhdGEYASABKAxIABIpCgVjbG9zZRgCIAEoCzIYLmVuZ3JhbS5hcHAudjEuUG9ydENsb3NlSABCBwoFZnJhbWUiLAoIUG9ydE9wZW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwb3J0GAIgASgNIgsKCVBvcnRDbG9zZTKoEwoOU2Vzc2lvblNlcnZpY2USVwoMTGlzdFNlc3Npb25zEiIuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbnNSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbnNSZXNwb25zZRJaCg1DcmVhdGVTZXNzaW9uEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlU2Vzc2lvblJlc3BvbnNlElEKCkdldFNlc3Npb24SIC5lbmdyYW0uYXBwLnYxLkdldFNlc3Npb25SZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uUmVzcG9uc2USWgoNRGVsZXRlU2Vzc2lvbhIjLmVuZ3JhbS5hcHAudjEuRGVsZXRlU2Vzc2lvblJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkRlbGV0ZVNlc3Npb25SZXNwb25zZRJRCgpTZW5kUHJvbXB0EiAuZW5ncmFtLmFwcC52MS5TZW5kUHJvbXB0UmVxdWVzdBohLmVuZ3JhbS5hcHAudjEuU2VuZFByb21wdFJlc3BvbnNlEk4KCUludGVycnVwdBIfLmVuZ3JhbS5hcHAudjEuSW50ZXJydXB0UmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuSW50ZXJydXB0UmVzcG9uc2USYwoQRWRpdFF1ZXVlZFByb21wdBImLmVuZ3JhbS5hcHAudjEuRWRpdFF1ZXVlZFByb21wdFJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkVkaXRRdWV1ZWRQcm9tcHRSZXNwb25zZRJsChNEZXF1ZXVlUXVldWVkUHJvbXB0EikuZW5ncmFtLmFwcC52MS5EZXF1ZXVlUXVldWVkUHJvbXB0UmVxdWVzdBoqLmVuZ3JhbS5hcHAudjEuRGVxdWV1ZVF1ZXVlZFByb21wdFJlc3BvbnNlEmMKEENvbXBsZXRlVG9vbENhbGwSJi5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlVG9vbENhbGxSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5Db21wbGV0ZVRvb2xDYWxsUmVzcG9uc2USZgoRR2V0TWNwV3JpdGVQb2xpY3kSJy5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZRJRCgxTdHJlYW1FdmVudHMSIi5lbmdyYW0uYXBwLnYxLlN0cmVhbUV2ZW50c1JlcXVlc3QaGy5lbmdyYW0uYXBwLnYxLlNlc3Npb25FdmVudDABEmYKEUxpc3RTZXNzaW9uRXZlbnRzEicuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbkV2ZW50c1JlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLkxpc3RTZXNzaW9uRXZlbnRzUmVzcG9uc2USPwoERXhlYxIaLmVuZ3JhbS5hcHAudjEuRXhlY1JlcXVlc3QaGS5lbmdyYW0uYXBwLnYxLkV4ZWNPdXRwdXQwARJRCgpDYW5jZWxFeGVjEiAuZW5ncmFtLmFwcC52MS5DYW5jZWxFeGVjUmVxdWVzdBohLmVuZ3JhbS5hcHAudjEuQ2FuY2VsRXhlY1Jlc3BvbnNlElAKCVdyaXRlRmlsZRIfLmVuZ3JhbS5hcHAudjEuV3JpdGVGaWxlUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuV3JpdGVGaWxlUmVzcG9uc2UoARJNCghSZWFkRmlsZRIeLmVuZ3JhbS5hcHAudjEuUmVhZEZpbGVSZXF1ZXN0Gh8uZW5ncmFtLmFwcC52MS5SZWFkRmlsZVJlc3BvbnNlMAESTgoJQ29weUZpbGVzEh8uZW5ncmFtLmFwcC52MS5Db3B5RmlsZXNSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5Db3B5RmlsZXNSZXNwb25zZRJFCgZHZXRMb2cSHC5lbmdyYW0uYXBwLnYxLkdldExvZ1JlcXVlc3QaHS5lbmdyYW0uYXBwLnYxLkdldExvZ1Jlc3BvbnNlEksKCFNuYXBzaG90Eh4uZW5ncmFtLmFwcC52MS5TbmFwc2hvdFJlcXVlc3QaHy5lbmdyYW0uYXBwLnYxLlNuYXBzaG90UmVzcG9uc2USWgoNRW5zdXJlQnJvd3NlchIjLmVuZ3JhbS5hcHAudjEuRW5zdXJlQnJvd3NlclJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkVuc3VyZUJyb3dzZXJSZXNwb25zZRJOCglFbnN1cmVJZGUSHy5lbmdyYW0uYXBwLnYxLkVuc3VyZUlkZVJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLkVuc3VyZUlkZVJlc3BvbnNlEkUKBlJlc3VtZRIcLmVuZ3JhbS5hcHAudjEuUmVzdW1lUmVxdWVzdBodLmVuZ3JhbS5hcHAudjEuUmVzdW1lUmVzcG9uc2USUQoKRXZpY3RMb2NhbBIgLmVuZ3JhbS5hcHAudjEuRXZpY3RMb2NhbFJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkV2aWN0TG9jYWxSZXNwb25zZRJOCglFdmljdElkbGUSHy5lbmdyYW0uYXBwLnYxLkV2aWN0SWRsZVJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLkV2aWN0SWRsZVJlc3BvbnNlElQKC0dldENvd1N0YXRlEiEuZW5ncmFtLmFwcC52MS5HZXRDb3dTdGF0ZVJlcXVlc3QaIi5lbmdyYW0uYXBwLnYxLkdldENvd1N0YXRlUmVzcG9uc2USYAoPTGlzdENoZWNrcG9pbnRzEiUuZW5ncmFtLmFwcC52MS5MaXN0Q2hlY2twb2ludHNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q2hlY2twb2ludHNSZXNwb25zZRJWCgtHZXRBcnRpZmFjdBIhLmVuZ3JhbS5hcHAudjEuR2V0QXJ0aWZhY3RSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRBcnRpZmFjdFJlc3BvbnNlMAESdQoWQ3JlYXRlQXJ0aWZhY3RGcm9tUGF0aBIsLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQXJ0aWZhY3RGcm9tUGF0aFJlcXVlc3QaLS5lbmdyYW0uYXBwLnYxLkNyZWF0ZUFydGlmYWN0RnJvbVBhdGhSZXNwb25zZTJlChFTaGVsbFJlbGF5U2VydmljZRJQCgVSZWxheRIgLmVuZ3JhbS5hcHAudjEuUmVsYXlTaGVsbFJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLlJlbGF5U2hlbGxSZXNwb25zZSgBMAEyYgoQUG9ydFJlbGF5U2VydmljZRJOCgVSZWxheRIfLmVuZ3JhbS5hcHAudjEuUmVsYXlQb3J0UmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuUmVsYXlQb3J0UmVzcG9uc2UoATABYgZwcm90bzM", [file_engram_app_v1_oauth]); /** * @generated from message engram.app.v1.ListSessionsRequest @@ -684,6 +684,196 @@ export type DequeueQueuedPromptResponse = Message<"engram.app.v1.DequeueQueuedPr export const DequeueQueuedPromptResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_session, 19); +/** + * @generated from message engram.app.v1.McpWritePolicyRef + */ +export type McpWritePolicyRef = Message<"engram.app.v1.McpWritePolicyRef"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: int64 policy_revision = 2; + */ + policyRevision: bigint; + + /** + * @generated from field: int64 binding_epoch = 3; + */ + bindingEpoch: bigint; +}; + +/** + * Describes the message engram.app.v1.McpWritePolicyRef. + * Use `create(McpWritePolicyRefSchema)` to create a new message. + */ +export const McpWritePolicyRefSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 20); + +/** + * @generated from message engram.app.v1.McpTrustedWritePolicy + */ +export type McpTrustedWritePolicy = Message<"engram.app.v1.McpTrustedWritePolicy"> & { + /** + * @generated from field: engram.app.v1.McpWritePolicyRef ref = 1; + */ + ref?: McpWritePolicyRef; + + /** + * @generated from field: string write_eligibility = 2; + */ + writeEligibility: string; + + /** + * @generated from field: string cause = 3; + */ + cause: string; + + /** + * @generated from field: string accepted_input_id = 4; + */ + acceptedInputId: string; + + /** + * @generated from field: optional string authenticated_actor_id = 5; + */ + authenticatedActorId?: string; + + /** + * @generated from field: optional string prompt_id = 6; + */ + promptId?: string; + + /** + * @generated from field: optional string plan_tool_call_id = 7; + */ + planToolCallId?: string; + + /** + * @generated from field: optional string plan_request_digest = 8; + */ + planRequestDigest?: string; + + /** + * @generated from field: repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 9; + */ + ancestorPolicyRefs: McpWritePolicyRef[]; + + /** + * @generated from field: bool current = 10; + */ + current: boolean; + + /** + * @generated from field: bool initialized = 11; + */ + initialized: boolean; +}; + +/** + * Describes the message engram.app.v1.McpTrustedWritePolicy. + * Use `create(McpTrustedWritePolicySchema)` to create a new message. + */ +export const McpTrustedWritePolicySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 21); + +/** + * @generated from message engram.app.v1.McpPlanReview + */ +export type McpPlanReview = Message<"engram.app.v1.McpPlanReview"> & { + /** + * @generated from field: engram.app.v1.McpWritePolicyRef policy_ref = 1; + */ + policyRef?: McpWritePolicyRef; + + /** + * @generated from field: string tool_call_id = 2; + */ + toolCallId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: int64 event_idx = 4; + */ + eventIdx: bigint; + + /** + * @generated from field: string request_digest = 5; + */ + requestDigest: string; + + /** + * @generated from field: string args_json = 6; + */ + argsJson: string; +}; + +/** + * Describes the message engram.app.v1.McpPlanReview. + * Use `create(McpPlanReviewSchema)` to create a new message. + */ +export const McpPlanReviewSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 22); + +/** + * @generated from message engram.app.v1.GetMcpWritePolicyRequest + */ +export type GetMcpWritePolicyRequest = Message<"engram.app.v1.GetMcpWritePolicyRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: optional string plan_tool_call_id = 2; + */ + planToolCallId?: string; +}; + +/** + * Describes the message engram.app.v1.GetMcpWritePolicyRequest. + * Use `create(GetMcpWritePolicyRequestSchema)` to create a new message. + */ +export const GetMcpWritePolicyRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 23); + +/** + * @generated from message engram.app.v1.GetMcpWritePolicyResponse + */ +export type GetMcpWritePolicyResponse = Message<"engram.app.v1.GetMcpWritePolicyResponse"> & { + /** + * @generated from field: engram.app.v1.McpTrustedWritePolicy policy = 1; + */ + policy?: McpTrustedWritePolicy; + + /** + * @generated from field: engram.app.v1.McpPlanReview plan_review = 2; + */ + planReview?: McpPlanReview; + + /** + * @generated from field: string reason = 3; + */ + reason: string; + + /** + * @generated from field: repeated string allowed_actions = 4; + */ + allowedActions: string[]; +}; + +/** + * Describes the message engram.app.v1.GetMcpWritePolicyResponse. + * Use `create(GetMcpWritePolicyResponseSchema)` to create a new message. + */ +export const GetMcpWritePolicyResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 24); + /** * @generated from message engram.app.v1.CompleteToolCallRequest */ @@ -708,6 +898,13 @@ export type CompleteToolCallRequest = Message<"engram.app.v1.CompleteToolCallReq * @generated from field: string result_json = 3; */ resultJson: string; + + /** + * Optional exact server review. Only the authenticated human handler consumes it. + * + * @generated from field: engram.app.v1.McpPlanReview expected_mcp_plan_review = 4; + */ + expectedMcpPlanReview?: McpPlanReview; }; /** @@ -715,7 +912,7 @@ export type CompleteToolCallRequest = Message<"engram.app.v1.CompleteToolCallReq * Use `create(CompleteToolCallRequestSchema)` to create a new message. */ export const CompleteToolCallRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 20); + messageDesc(file_engram_app_v1_session, 25); /** * @generated from message engram.app.v1.CompleteToolCallResponse @@ -737,7 +934,7 @@ export type CompleteToolCallResponse = Message<"engram.app.v1.CompleteToolCallRe * Use `create(CompleteToolCallResponseSchema)` to create a new message. */ export const CompleteToolCallResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 21); + messageDesc(file_engram_app_v1_session, 26); /** * @generated from message engram.app.v1.StreamEventsRequest @@ -774,7 +971,7 @@ export type StreamEventsRequest = Message<"engram.app.v1.StreamEventsRequest"> & * Use `create(StreamEventsRequestSchema)` to create a new message. */ export const StreamEventsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 22); + messageDesc(file_engram_app_v1_session, 27); /** * The typed event ENVELOPE. payload_json is NORMATIVELY the JSON object @@ -812,7 +1009,7 @@ export type SessionEvent = Message<"engram.app.v1.SessionEvent"> & { * Use `create(SessionEventSchema)` to create a new message. */ export const SessionEventSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 23); + messageDesc(file_engram_app_v1_session, 28); /** * @generated from message engram.app.v1.ListSessionEventsRequest @@ -878,7 +1075,7 @@ export type ListSessionEventsRequest = Message<"engram.app.v1.ListSessionEventsR * Use `create(ListSessionEventsRequestSchema)` to create a new message. */ export const ListSessionEventsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 24); + messageDesc(file_engram_app_v1_session, 29); /** * @generated from message engram.app.v1.ListSessionEventsResponse @@ -907,7 +1104,7 @@ export type ListSessionEventsResponse = Message<"engram.app.v1.ListSessionEvents * Use `create(ListSessionEventsResponseSchema)` to create a new message. */ export const ListSessionEventsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 25); + messageDesc(file_engram_app_v1_session, 30); /** * @generated from message engram.app.v1.ExecRequest @@ -977,7 +1174,7 @@ export type ExecRequest = Message<"engram.app.v1.ExecRequest"> & { * Use `create(ExecRequestSchema)` to create a new message. */ export const ExecRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 26); + messageDesc(file_engram_app_v1_session, 31); /** * @generated from message engram.app.v1.CancelExecRequest @@ -999,7 +1196,7 @@ export type CancelExecRequest = Message<"engram.app.v1.CancelExecRequest"> & { * Use `create(CancelExecRequestSchema)` to create a new message. */ export const CancelExecRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 27); + messageDesc(file_engram_app_v1_session, 32); /** * @generated from message engram.app.v1.CancelExecResponse @@ -1012,7 +1209,7 @@ export type CancelExecResponse = Message<"engram.app.v1.CancelExecResponse"> & { * Use `create(CancelExecResponseSchema)` to create a new message. */ export const CancelExecResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 28); + messageDesc(file_engram_app_v1_session, 33); /** * @generated from message engram.app.v1.WriteFileMetadata @@ -1055,7 +1252,7 @@ export type WriteFileMetadata = Message<"engram.app.v1.WriteFileMetadata"> & { * Use `create(WriteFileMetadataSchema)` to create a new message. */ export const WriteFileMetadataSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 29); + messageDesc(file_engram_app_v1_session, 34); /** * @generated from message engram.app.v1.WriteFileRequest @@ -1084,7 +1281,7 @@ export type WriteFileRequest = Message<"engram.app.v1.WriteFileRequest"> & { * Use `create(WriteFileRequestSchema)` to create a new message. */ export const WriteFileRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 30); + messageDesc(file_engram_app_v1_session, 35); /** * @generated from message engram.app.v1.WriteFileResponse @@ -1111,7 +1308,7 @@ export type WriteFileResponse = Message<"engram.app.v1.WriteFileResponse"> & { * Use `create(WriteFileResponseSchema)` to create a new message. */ export const WriteFileResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 31); + messageDesc(file_engram_app_v1_session, 36); /** * @generated from message engram.app.v1.ReadFileRequest @@ -1133,7 +1330,7 @@ export type ReadFileRequest = Message<"engram.app.v1.ReadFileRequest"> & { * Use `create(ReadFileRequestSchema)` to create a new message. */ export const ReadFileRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 32); + messageDesc(file_engram_app_v1_session, 37); /** * @generated from message engram.app.v1.ReadFileMetadata @@ -1165,7 +1362,7 @@ export type ReadFileMetadata = Message<"engram.app.v1.ReadFileMetadata"> & { * Use `create(ReadFileMetadataSchema)` to create a new message. */ export const ReadFileMetadataSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 33); + messageDesc(file_engram_app_v1_session, 38); /** * @generated from message engram.app.v1.ReadFileResponse @@ -1194,7 +1391,7 @@ export type ReadFileResponse = Message<"engram.app.v1.ReadFileResponse"> & { * Use `create(ReadFileResponseSchema)` to create a new message. */ export const ReadFileResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 34); + messageDesc(file_engram_app_v1_session, 39); /** * @generated from message engram.app.v1.CopyFilesRequest @@ -1221,7 +1418,7 @@ export type CopyFilesRequest = Message<"engram.app.v1.CopyFilesRequest"> & { * Use `create(CopyFilesRequestSchema)` to create a new message. */ export const CopyFilesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 35); + messageDesc(file_engram_app_v1_session, 40); /** * @generated from message engram.app.v1.CopyFileResult @@ -1248,7 +1445,7 @@ export type CopyFileResult = Message<"engram.app.v1.CopyFileResult"> & { * Use `create(CopyFileResultSchema)` to create a new message. */ export const CopyFileResultSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 36); + messageDesc(file_engram_app_v1_session, 41); /** * @generated from message engram.app.v1.CopyFilesResponse @@ -1265,7 +1462,7 @@ export type CopyFilesResponse = Message<"engram.app.v1.CopyFilesResponse"> & { * Use `create(CopyFilesResponseSchema)` to create a new message. */ export const CopyFilesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 37); + messageDesc(file_engram_app_v1_session, 42); /** * engram_core::types::ExecEvent is authoritative for the stream body: @@ -1317,7 +1514,7 @@ export type ExecOutput = Message<"engram.app.v1.ExecOutput"> & { * Use `create(ExecOutputSchema)` to create a new message. */ export const ExecOutputSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 38); + messageDesc(file_engram_app_v1_session, 43); /** * @generated from message engram.app.v1.ExecStarted @@ -1334,7 +1531,7 @@ export type ExecStarted = Message<"engram.app.v1.ExecStarted"> & { * Use `create(ExecStartedSchema)` to create a new message. */ export const ExecStartedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 39); + messageDesc(file_engram_app_v1_session, 44); /** * @generated from message engram.app.v1.ExecExit @@ -1362,7 +1559,7 @@ export type ExecExit = Message<"engram.app.v1.ExecExit"> & { * Use `create(ExecExitSchema)` to create a new message. */ export const ExecExitSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 40); + messageDesc(file_engram_app_v1_session, 45); /** * engram_core::types::ExecRusage. wall_ms is coordinator-measured wall @@ -1398,7 +1595,7 @@ export type ExecRusage = Message<"engram.app.v1.ExecRusage"> & { * Use `create(ExecRusageSchema)` to create a new message. */ export const ExecRusageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 41); + messageDesc(file_engram_app_v1_session, 46); /** * @generated from message engram.app.v1.GetLogRequest @@ -1441,7 +1638,7 @@ export type GetLogRequest = Message<"engram.app.v1.GetLogRequest"> & { * Use `create(GetLogRequestSchema)` to create a new message. */ export const GetLogRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 42); + messageDesc(file_engram_app_v1_session, 47); /** * @generated from message engram.app.v1.GetLogResponse @@ -1468,7 +1665,7 @@ export type GetLogResponse = Message<"engram.app.v1.GetLogResponse"> & { * Use `create(GetLogResponseSchema)` to create a new message. */ export const GetLogResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 43); + messageDesc(file_engram_app_v1_session, 48); /** * @generated from message engram.app.v1.ConversationEntry @@ -1504,7 +1701,7 @@ export type ConversationEntry = Message<"engram.app.v1.ConversationEntry"> & { * Use `create(ConversationEntrySchema)` to create a new message. */ export const ConversationEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 44); + messageDesc(file_engram_app_v1_session, 49); /** * @generated from message engram.app.v1.SnapshotRequest @@ -1521,7 +1718,7 @@ export type SnapshotRequest = Message<"engram.app.v1.SnapshotRequest"> & { * Use `create(SnapshotRequestSchema)` to create a new message. */ export const SnapshotRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 45); + messageDesc(file_engram_app_v1_session, 50); /** * @generated from message engram.app.v1.SnapshotResponse @@ -1555,7 +1752,7 @@ export type SnapshotResponse = Message<"engram.app.v1.SnapshotResponse"> & { * Use `create(SnapshotResponseSchema)` to create a new message. */ export const SnapshotResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 46); + messageDesc(file_engram_app_v1_session, 51); /** * @generated from message engram.app.v1.EnsureBrowserRequest @@ -1572,7 +1769,7 @@ export type EnsureBrowserRequest = Message<"engram.app.v1.EnsureBrowserRequest"> * Use `create(EnsureBrowserRequestSchema)` to create a new message. */ export const EnsureBrowserRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 47); + messageDesc(file_engram_app_v1_session, 52); /** * @generated from message engram.app.v1.EnsureBrowserResponse @@ -1592,7 +1789,7 @@ export type EnsureBrowserResponse = Message<"engram.app.v1.EnsureBrowserResponse * Use `create(EnsureBrowserResponseSchema)` to create a new message. */ export const EnsureBrowserResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 48); + messageDesc(file_engram_app_v1_session, 53); /** * @generated from message engram.app.v1.EnsureIdeRequest @@ -1609,7 +1806,7 @@ export type EnsureIdeRequest = Message<"engram.app.v1.EnsureIdeRequest"> & { * Use `create(EnsureIdeRequestSchema)` to create a new message. */ export const EnsureIdeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 49); + messageDesc(file_engram_app_v1_session, 54); /** * @generated from message engram.app.v1.EnsureIdeResponse @@ -1629,7 +1826,7 @@ export type EnsureIdeResponse = Message<"engram.app.v1.EnsureIdeResponse"> & { * Use `create(EnsureIdeResponseSchema)` to create a new message. */ export const EnsureIdeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 50); + messageDesc(file_engram_app_v1_session, 55); /** * @generated from message engram.app.v1.ResumeRequest @@ -1646,7 +1843,7 @@ export type ResumeRequest = Message<"engram.app.v1.ResumeRequest"> & { * Use `create(ResumeRequestSchema)` to create a new message. */ export const ResumeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 51); + messageDesc(file_engram_app_v1_session, 56); /** * Same shape as SnapshotResponse (the HTTP resume handler reuses it); @@ -1681,7 +1878,7 @@ export type ResumeResponse = Message<"engram.app.v1.ResumeResponse"> & { * Use `create(ResumeResponseSchema)` to create a new message. */ export const ResumeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 52); + messageDesc(file_engram_app_v1_session, 57); /** * @generated from message engram.app.v1.EvictLocalRequest @@ -1698,7 +1895,7 @@ export type EvictLocalRequest = Message<"engram.app.v1.EvictLocalRequest"> & { * Use `create(EvictLocalRequestSchema)` to create a new message. */ export const EvictLocalRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 53); + messageDesc(file_engram_app_v1_session, 58); /** * Today's HTTP handler returns 202 Accepted with no body. @@ -1713,7 +1910,7 @@ export type EvictLocalResponse = Message<"engram.app.v1.EvictLocalResponse"> & { * Use `create(EvictLocalResponseSchema)` to create a new message. */ export const EvictLocalResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 54); + messageDesc(file_engram_app_v1_session, 59); /** * @generated from message engram.app.v1.EvictIdleRequest @@ -1730,7 +1927,7 @@ export type EvictIdleRequest = Message<"engram.app.v1.EvictIdleRequest"> & { * Use `create(EvictIdleRequestSchema)` to create a new message. */ export const EvictIdleRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 55); + messageDesc(file_engram_app_v1_session, 60); /** * Mirrors api/admin.rs EvictIdleResponse. The synchronous pipeline has @@ -1757,7 +1954,7 @@ export type EvictIdleResponse = Message<"engram.app.v1.EvictIdleResponse"> & { * Use `create(EvictIdleResponseSchema)` to create a new message. */ export const EvictIdleResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 56); + messageDesc(file_engram_app_v1_session, 61); /** * @generated from message engram.app.v1.GetCowStateRequest @@ -1774,7 +1971,7 @@ export type GetCowStateRequest = Message<"engram.app.v1.GetCowStateRequest"> & { * Use `create(GetCowStateRequestSchema)` to create a new message. */ export const GetCowStateRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 57); + messageDesc(file_engram_app_v1_session, 62); /** * @generated from message engram.app.v1.GetCowStateResponse @@ -1799,7 +1996,7 @@ export type GetCowStateResponse = Message<"engram.app.v1.GetCowStateResponse"> & * Use `create(GetCowStateResponseSchema)` to create a new message. */ export const GetCowStateResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 58); + messageDesc(file_engram_app_v1_session, 63); /** * Mirrors engram_coordinator::cow_state::CowStateView. One row per @@ -1892,7 +2089,7 @@ export type CowStateView = Message<"engram.app.v1.CowStateView"> & { * Use `create(CowStateViewSchema)` to create a new message. */ export const CowStateViewSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 59); + messageDesc(file_engram_app_v1_session, 64); /** * @generated from message engram.app.v1.ListCheckpointsRequest @@ -1909,7 +2106,7 @@ export type ListCheckpointsRequest = Message<"engram.app.v1.ListCheckpointsReque * Use `create(ListCheckpointsRequestSchema)` to create a new message. */ export const ListCheckpointsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 60); + messageDesc(file_engram_app_v1_session, 65); /** * @generated from message engram.app.v1.ListCheckpointsResponse @@ -1934,7 +2131,7 @@ export type ListCheckpointsResponse = Message<"engram.app.v1.ListCheckpointsResp * Use `create(ListCheckpointsResponseSchema)` to create a new message. */ export const ListCheckpointsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 61); + messageDesc(file_engram_app_v1_session, 66); /** * ADR 0028 A.log: one checkpoint in a session's chain. @@ -1986,7 +2183,7 @@ export type CheckpointSummary = Message<"engram.app.v1.CheckpointSummary"> & { * Use `create(CheckpointSummarySchema)` to create a new message. */ export const CheckpointSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 62); + messageDesc(file_engram_app_v1_session, 67); /** * @generated from message engram.app.v1.GetArtifactRequest @@ -2008,7 +2205,7 @@ export type GetArtifactRequest = Message<"engram.app.v1.GetArtifactRequest"> & { * Use `create(GetArtifactRequestSchema)` to create a new message. */ export const GetArtifactRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 63); + messageDesc(file_engram_app_v1_session, 68); /** * @generated from message engram.app.v1.GetArtifactResponse @@ -2039,7 +2236,7 @@ export type GetArtifactResponse = Message<"engram.app.v1.GetArtifactResponse"> & * Use `create(GetArtifactResponseSchema)` to create a new message. */ export const GetArtifactResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 64); + messageDesc(file_engram_app_v1_session, 69); /** * @generated from message engram.app.v1.ArtifactMetadata @@ -2069,7 +2266,7 @@ export type ArtifactMetadata = Message<"engram.app.v1.ArtifactMetadata"> & { * Use `create(ArtifactMetadataSchema)` to create a new message. */ export const ArtifactMetadataSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 65); + messageDesc(file_engram_app_v1_session, 70); /** * ADR 0026 trusted operator file pull: capture ANY file in the session @@ -2108,7 +2305,7 @@ export type CreateArtifactFromPathRequest = Message<"engram.app.v1.CreateArtifac * Use `create(CreateArtifactFromPathRequestSchema)` to create a new message. */ export const CreateArtifactFromPathRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 66); + messageDesc(file_engram_app_v1_session, 71); /** * @generated from message engram.app.v1.CreateArtifactFromPathResponse @@ -2135,7 +2332,7 @@ export type CreateArtifactFromPathResponse = Message<"engram.app.v1.CreateArtifa * Use `create(CreateArtifactFromPathResponseSchema)` to create a new message. */ export const CreateArtifactFromPathResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 67); + messageDesc(file_engram_app_v1_session, 72); /** * Frame kinds mirror the WS frames / host ProxyShellMessage 1:1. Request @@ -2194,7 +2391,7 @@ export type RelayShellRequest = Message<"engram.app.v1.RelayShellRequest"> & { * Use `create(RelayShellRequestSchema)` to create a new message. */ export const RelayShellRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 68); + messageDesc(file_engram_app_v1_session, 73); /** * @generated from message engram.app.v1.RelayShellResponse @@ -2241,7 +2438,7 @@ export type RelayShellResponse = Message<"engram.app.v1.RelayShellResponse"> & { * Use `create(RelayShellResponseSchema)` to create a new message. */ export const RelayShellResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 69); + messageDesc(file_engram_app_v1_session, 74); /** * @generated from message engram.app.v1.ShellOpen @@ -2258,7 +2455,7 @@ export type ShellOpen = Message<"engram.app.v1.ShellOpen"> & { * Use `create(ShellOpenSchema)` to create a new message. */ export const ShellOpenSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 70); + messageDesc(file_engram_app_v1_session, 75); /** * @generated from message engram.app.v1.ShellClose @@ -2280,7 +2477,7 @@ export type ShellClose = Message<"engram.app.v1.ShellClose"> & { * Use `create(ShellCloseSchema)` to create a new message. */ export const ShellCloseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 71); + messageDesc(file_engram_app_v1_session, 76); /** * Raw opaque bytes; no frame-kind taxonomy (cf. RelayShell*). Request @@ -2321,7 +2518,7 @@ export type RelayPortRequest = Message<"engram.app.v1.RelayPortRequest"> & { * Use `create(RelayPortRequestSchema)` to create a new message. */ export const RelayPortRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 72); + messageDesc(file_engram_app_v1_session, 77); /** * @generated from message engram.app.v1.RelayPortResponse @@ -2350,7 +2547,7 @@ export type RelayPortResponse = Message<"engram.app.v1.RelayPortResponse"> & { * Use `create(RelayPortResponseSchema)` to create a new message. */ export const RelayPortResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 73); + messageDesc(file_engram_app_v1_session, 78); /** * @generated from message engram.app.v1.PortOpen @@ -2374,7 +2571,7 @@ export type PortOpen = Message<"engram.app.v1.PortOpen"> & { * Use `create(PortOpenSchema)` to create a new message. */ export const PortOpenSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 74); + messageDesc(file_engram_app_v1_session, 79); /** * Clean teardown sentinel (see ProxyPortClose in host_service.proto). @@ -2389,7 +2586,7 @@ export type PortClose = Message<"engram.app.v1.PortClose"> & { * Use `create(PortCloseSchema)` to create a new message. */ export const PortCloseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 75); + messageDesc(file_engram_app_v1_session, 80); /** * The orchestrator-facing app contract (ADR 0051 §2.3, rev 2026-06-10). @@ -2482,6 +2679,14 @@ export const SessionService: GenService<{ input: typeof CompleteToolCallRequestSchema; output: typeof CompleteToolCallResponseSchema; }, + /** + * @generated from rpc engram.app.v1.SessionService.GetMcpWritePolicy + */ + getMcpWritePolicy: { + methodKind: "unary"; + input: typeof GetMcpWritePolicyRequestSchema; + output: typeof GetMcpWritePolicyResponseSchema; + }, /** * Streams the event envelope itself, not a wrapped Response message. * buf:lint:ignore RPC_RESPONSE_STANDARD_NAME diff --git a/orchestrator/src/gen/engram/mcp/v1/control_pb.ts b/orchestrator/src/gen/engram/mcp/v1/control_pb.ts index c7c78e197..31061b23b 100644 --- a/orchestrator/src/gen/engram/mcp/v1/control_pb.ts +++ b/orchestrator/src/gen/engram/mcp/v1/control_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/control.proto. */ export const file_engram_mcp_v1_control: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDIoEBChpDYW5jZWxBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJEhAKCGFjdG9yX2lkGAUgASgJInAKG0NhbmNlbEF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJKowBCg5DcmVkZW50aWFsS2luZBIfChtDUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIbChdDUkVERU5USUFMX0tJTkRfQVBJX0tFWRABEiEKHUNSRURFTlRJQUxfS0lORF9DTElFTlRfU0VDUkVUEAISGQoVQ1JFREVOVElBTF9LSU5EX09BVVRIEAMy/gQKEUNyZWRlbnRpYWxDb250cm9sEk0KCVB1dFNlY3JldBIfLmVuZ3JhbS5tY3AudjEuUHV0U2VjcmV0UmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmwKE0NhbmNlbEF1dGhvcml6YXRpb24SKS5lbmdyYW0ubWNwLnYxLkNhbmNlbEF1dGhvcml6YXRpb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DYW5jZWxBdXRob3JpemF0aW9uUmVzcG9uc2USZQoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEk0KCUdldFN0YXR1cxIfLmVuZ3JhbS5tY3AudjEuR2V0U3RhdHVzUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJHCgZSZXZva2USHC5lbmdyYW0ubWNwLnYxLlJldm9rZVJlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); + fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDIoEBChpDYW5jZWxBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJEhAKCGFjdG9yX2lkGAUgASgJInAKG0NhbmNlbEF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJIkQKEVB1dFNlY3JldFJlc3BvbnNlEi8KBnN0YXR1cxgBIAEoCzIfLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cyJQCh1Db21wbGV0ZUF1dGhvcml6YXRpb25SZXNwb25zZRIvCgZzdGF0dXMYASABKAsyHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMiRAoRR2V0U3RhdHVzUmVzcG9uc2USLwoGc3RhdHVzGAEgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzIkEKDlJldm9rZVJlc3BvbnNlEi8KBnN0YXR1cxgBIAEoCzIfLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cyqMAQoOQ3JlZGVudGlhbEtpbmQSHwobQ1JFREVOVElBTF9LSU5EX1VOU1BFQ0lGSUVEEAASGwoXQ1JFREVOVElBTF9LSU5EX0FQSV9LRVkQARIhCh1DUkVERU5USUFMX0tJTkRfQ0xJRU5UX1NFQ1JFVBACEhkKFUNSRURFTlRJQUxfS0lORF9PQVVUSBADMpIFChhDcmVkZW50aWFsQ29udHJvbFNlcnZpY2USTgoJUHV0U2VjcmV0Eh8uZW5ncmFtLm1jcC52MS5QdXRTZWNyZXRSZXF1ZXN0GiAuZW5ncmFtLm1jcC52MS5QdXRTZWNyZXRSZXNwb25zZRJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmwKE0NhbmNlbEF1dGhvcml6YXRpb24SKS5lbmdyYW0ubWNwLnYxLkNhbmNlbEF1dGhvcml6YXRpb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DYW5jZWxBdXRob3JpemF0aW9uUmVzcG9uc2UScgoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXNwb25zZRJOCglHZXRTdGF0dXMSHy5lbmdyYW0ubWNwLnYxLkdldFN0YXR1c1JlcXVlc3QaIC5lbmdyYW0ubWNwLnYxLkdldFN0YXR1c1Jlc3BvbnNlEkUKBlJldm9rZRIcLmVuZ3JhbS5tY3AudjEuUmV2b2tlUmVxdWVzdBodLmVuZ3JhbS5tY3AudjEuUmV2b2tlUmVzcG9uc2USQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); /** * @generated from message engram.mcp.v1.Destination @@ -746,6 +746,74 @@ export type CancelAuthorizationResponse = Message<"engram.mcp.v1.CancelAuthoriza export const CancelAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_control, 18); +/** + * @generated from message engram.mcp.v1.PutSecretResponse + */ +export type PutSecretResponse = Message<"engram.mcp.v1.PutSecretResponse"> & { + /** + * @generated from field: engram.mcp.v1.CredentialStatus status = 1; + */ + status?: CredentialStatus; +}; + +/** + * Describes the message engram.mcp.v1.PutSecretResponse. + * Use `create(PutSecretResponseSchema)` to create a new message. + */ +export const PutSecretResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 19); + +/** + * @generated from message engram.mcp.v1.CompleteAuthorizationResponse + */ +export type CompleteAuthorizationResponse = Message<"engram.mcp.v1.CompleteAuthorizationResponse"> & { + /** + * @generated from field: engram.mcp.v1.CredentialStatus status = 1; + */ + status?: CredentialStatus; +}; + +/** + * Describes the message engram.mcp.v1.CompleteAuthorizationResponse. + * Use `create(CompleteAuthorizationResponseSchema)` to create a new message. + */ +export const CompleteAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 20); + +/** + * @generated from message engram.mcp.v1.GetStatusResponse + */ +export type GetStatusResponse = Message<"engram.mcp.v1.GetStatusResponse"> & { + /** + * @generated from field: engram.mcp.v1.CredentialStatus status = 1; + */ + status?: CredentialStatus; +}; + +/** + * Describes the message engram.mcp.v1.GetStatusResponse. + * Use `create(GetStatusResponseSchema)` to create a new message. + */ +export const GetStatusResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 21); + +/** + * @generated from message engram.mcp.v1.RevokeResponse + */ +export type RevokeResponse = Message<"engram.mcp.v1.RevokeResponse"> & { + /** + * @generated from field: engram.mcp.v1.CredentialStatus status = 1; + */ + status?: CredentialStatus; +}; + +/** + * Describes the message engram.mcp.v1.RevokeResponse. + * Use `create(RevokeResponseSchema)` to create a new message. + */ +export const RevokeResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 22); + /** * @generated from enum engram.mcp.v1.CredentialKind */ @@ -780,19 +848,19 @@ export const CredentialKindSchema: GenEnum = /*@__PURE__*/ /** * Internal protocol 1. Served only on the separate mTLS listener. * - * @generated from service engram.mcp.v1.CredentialControl + * @generated from service engram.mcp.v1.CredentialControlService */ -export const CredentialControl: GenService<{ +export const CredentialControlService: GenService<{ /** - * @generated from rpc engram.mcp.v1.CredentialControl.PutSecret + * @generated from rpc engram.mcp.v1.CredentialControlService.PutSecret */ putSecret: { methodKind: "unary"; input: typeof PutSecretRequestSchema; - output: typeof CredentialStatusSchema; + output: typeof PutSecretResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.BeginAuthorization + * @generated from rpc engram.mcp.v1.CredentialControlService.BeginAuthorization */ beginAuthorization: { methodKind: "unary"; @@ -800,7 +868,7 @@ export const CredentialControl: GenService<{ output: typeof BeginAuthorizationResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.CancelAuthorization + * @generated from rpc engram.mcp.v1.CredentialControlService.CancelAuthorization */ cancelAuthorization: { methodKind: "unary"; @@ -808,31 +876,31 @@ export const CredentialControl: GenService<{ output: typeof CancelAuthorizationResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.CompleteAuthorization + * @generated from rpc engram.mcp.v1.CredentialControlService.CompleteAuthorization */ completeAuthorization: { methodKind: "unary"; input: typeof CompleteAuthorizationRequestSchema; - output: typeof CredentialStatusSchema; + output: typeof CompleteAuthorizationResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.GetStatus + * @generated from rpc engram.mcp.v1.CredentialControlService.GetStatus */ getStatus: { methodKind: "unary"; input: typeof GetStatusRequestSchema; - output: typeof CredentialStatusSchema; + output: typeof GetStatusResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.Revoke + * @generated from rpc engram.mcp.v1.CredentialControlService.Revoke */ revoke: { methodKind: "unary"; input: typeof RevokeRequestSchema; - output: typeof CredentialStatusSchema; + output: typeof RevokeResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.Lease + * @generated from rpc engram.mcp.v1.CredentialControlService.Lease */ lease: { methodKind: "unary"; diff --git a/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts b/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts new file mode 100644 index 000000000..a2c12482d --- /dev/null +++ b/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts @@ -0,0 +1,273 @@ +// @generated by protoc-gen-es v2.2.5 with parameter "target=ts" +// @generated from file engram/mcp/v1/session_policy.proto (package engram.mcp.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { CreateSessionRequest, CreateSessionResponse, GetMcpWritePolicyResponse, McpPlanReview, McpTrustedWritePolicy, McpWritePolicyRef } from "../../app/v1/session_pb"; +import { file_engram_app_v1_session } from "../../app/v1/session_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file engram/mcp/v1/session_policy.proto. + */ +export const file_engram_mcp_v1_session_policy: GenFile = /*@__PURE__*/ + fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIo0CChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYiQQodVmFsaWRhdGVEaXNwYXRjaEZlbmNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGc3RhdHVzGAIgASgJIlQKG0NyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRI1CgdzZXNzaW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVzcG9uc2UiUgoXUmVhZFdyaXRlUG9saWN5UmVzcG9uc2USNwoFc3RhdGUYASABKAsyKC5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVzcG9uc2UiTgoWQWNjZXB0UGxhbkV4aXRSZXNwb25zZRI0CgZwb2xpY3kYASABKAsyJC5lbmdyYW0uYXBwLnYxLk1jcFRydXN0ZWRXcml0ZVBvbGljeTK5AwoUU2Vzc2lvblBvbGljeVNlcnZpY2USbAoTQ3JlYXRlUG9saWN5U2Vzc2lvbhIpLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlcXVlc3QaKi5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRJgCg9SZWFkV3JpdGVQb2xpY3kSJS5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlcXVlc3QaJi5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlc3BvbnNlEl0KDkFjY2VwdFBsYW5FeGl0EiQuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlcXVlc3QaJS5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVzcG9uc2UScgoVVmFsaWRhdGVEaXNwYXRjaEZlbmNlEisuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZWIGcHJvdG8z", [file_engram_app_v1_session]); + +/** + * @generated from message engram.mcp.v1.CreatePolicySessionRequest + */ +export type CreatePolicySessionRequest = Message<"engram.mcp.v1.CreatePolicySessionRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.app.v1.CreateSessionRequest create = 2; + */ + create?: CreateSessionRequest; + + /** + * @generated from field: string actor_id = 3; + */ + actorId: string; + + /** + * @generated from field: string input_id = 4; + */ + inputId: string; + + /** + * @generated from field: optional string parent_session_id = 5; + */ + parentSessionId?: string; +}; + +/** + * Describes the message engram.mcp.v1.CreatePolicySessionRequest. + * Use `create(CreatePolicySessionRequestSchema)` to create a new message. + */ +export const CreatePolicySessionRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 0); + +/** + * @generated from message engram.mcp.v1.ReadWritePolicyRequest + */ +export type ReadWritePolicyRequest = Message<"engram.mcp.v1.ReadWritePolicyRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: optional string plan_tool_call_id = 3; + */ + planToolCallId?: string; +}; + +/** + * Describes the message engram.mcp.v1.ReadWritePolicyRequest. + * Use `create(ReadWritePolicyRequestSchema)` to create a new message. + */ +export const ReadWritePolicyRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 1); + +/** + * @generated from message engram.mcp.v1.AcceptPlanExitRequest + */ +export type AcceptPlanExitRequest = Message<"engram.mcp.v1.AcceptPlanExitRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.app.v1.McpPlanReview expected = 2; + */ + expected?: McpPlanReview; + + /** + * @generated from field: string actor_id = 3; + */ + actorId: string; + + /** + * @generated from field: string result_json = 4; + */ + resultJson: string; +}; + +/** + * Describes the message engram.mcp.v1.AcceptPlanExitRequest. + * Use `create(AcceptPlanExitRequestSchema)` to create a new message. + */ +export const AcceptPlanExitRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 2); + +/** + * @generated from message engram.mcp.v1.ValidateDispatchFenceRequest + */ +export type ValidateDispatchFenceRequest = Message<"engram.mcp.v1.ValidateDispatchFenceRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 4; + */ + toolCallId: string; + + /** + * @generated from field: engram.app.v1.McpWritePolicyRef accepted_policy_ref = 5; + */ + acceptedPolicyRef?: McpWritePolicyRef; + + /** + * @generated from field: string accepted_write_eligibility = 6; + */ + acceptedWriteEligibility: string; + + /** + * @generated from field: repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 7; + */ + ancestorPolicyRefs: McpWritePolicyRef[]; +}; + +/** + * Describes the message engram.mcp.v1.ValidateDispatchFenceRequest. + * Use `create(ValidateDispatchFenceRequestSchema)` to create a new message. + */ +export const ValidateDispatchFenceRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 3); + +/** + * @generated from message engram.mcp.v1.ValidateDispatchFenceResponse + */ +export type ValidateDispatchFenceResponse = Message<"engram.mcp.v1.ValidateDispatchFenceResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string status = 2; + */ + status: string; +}; + +/** + * Describes the message engram.mcp.v1.ValidateDispatchFenceResponse. + * Use `create(ValidateDispatchFenceResponseSchema)` to create a new message. + */ +export const ValidateDispatchFenceResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 4); + +/** + * @generated from message engram.mcp.v1.CreatePolicySessionResponse + */ +export type CreatePolicySessionResponse = Message<"engram.mcp.v1.CreatePolicySessionResponse"> & { + /** + * @generated from field: engram.app.v1.CreateSessionResponse session = 1; + */ + session?: CreateSessionResponse; +}; + +/** + * Describes the message engram.mcp.v1.CreatePolicySessionResponse. + * Use `create(CreatePolicySessionResponseSchema)` to create a new message. + */ +export const CreatePolicySessionResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 5); + +/** + * @generated from message engram.mcp.v1.ReadWritePolicyResponse + */ +export type ReadWritePolicyResponse = Message<"engram.mcp.v1.ReadWritePolicyResponse"> & { + /** + * @generated from field: engram.app.v1.GetMcpWritePolicyResponse state = 1; + */ + state?: GetMcpWritePolicyResponse; +}; + +/** + * Describes the message engram.mcp.v1.ReadWritePolicyResponse. + * Use `create(ReadWritePolicyResponseSchema)` to create a new message. + */ +export const ReadWritePolicyResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 6); + +/** + * @generated from message engram.mcp.v1.AcceptPlanExitResponse + */ +export type AcceptPlanExitResponse = Message<"engram.mcp.v1.AcceptPlanExitResponse"> & { + /** + * @generated from field: engram.app.v1.McpTrustedWritePolicy policy = 1; + */ + policy?: McpTrustedWritePolicy; +}; + +/** + * Describes the message engram.mcp.v1.AcceptPlanExitResponse. + * Use `create(AcceptPlanExitResponseSchema)` to create a new message. + */ +export const AcceptPlanExitResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 7); + +/** + * Dedicated pinned mTLS only. Create reuses the ordinary creation core. + * + * @generated from service engram.mcp.v1.SessionPolicyService + */ +export const SessionPolicyService: GenService<{ + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.CreatePolicySession + */ + createPolicySession: { + methodKind: "unary"; + input: typeof CreatePolicySessionRequestSchema; + output: typeof CreatePolicySessionResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ReadWritePolicy + */ + readWritePolicy: { + methodKind: "unary"; + input: typeof ReadWritePolicyRequestSchema; + output: typeof ReadWritePolicyResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.AcceptPlanExit + */ + acceptPlanExit: { + methodKind: "unary"; + input: typeof AcceptPlanExitRequestSchema; + output: typeof AcceptPlanExitResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ValidateDispatchFence + */ + validateDispatchFence: { + methodKind: "unary"; + input: typeof ValidateDispatchFenceRequestSchema; + output: typeof ValidateDispatchFenceResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_engram_mcp_v1_session_policy, 0); + diff --git a/orchestrator/src/index.ts b/orchestrator/src/index.ts index c2285d4d2..450ea6cc1 100644 --- a/orchestrator/src/index.ts +++ b/orchestrator/src/index.ts @@ -1,3 +1,4 @@ +import { makeSessionPolicyOverrides } from "./mcp/session-policy.ts"; import { randomUUID } from "node:crypto"; import { Hono } from "hono"; import { makeHttpLog } from "./http-log.ts"; @@ -578,7 +579,7 @@ const server = buildServer( const response = await controlPlaneSessions.getSession({ sessionId }); await productionSpecProjection.preparePrompt(sessionId, response.session?.status ?? ""); }, - }); + }, makeSessionPolicyOverrides()); }, // Raw WS-upgrade hooks, tried in order: the preview proxy first // (Host-keyed — a preview host is a different origin, so it wins diff --git a/orchestrator/src/mcp/control-client.ts b/orchestrator/src/mcp/control-client.ts index a63e31e2d..13920c6d2 100644 --- a/orchestrator/src/mcp/control-client.ts +++ b/orchestrator/src/mcp/control-client.ts @@ -1,9 +1,34 @@ import { createClient, ConnectError, Code, type Client } from "@connectrpc/connect"; import { createGrpcTransport } from "@connectrpc/connect-node"; import { checkServerIdentity } from "node:tls"; -import { CredentialControl } from "../gen/engram/mcp/v1/control_pb.ts"; +import { + CredentialControlService, + type CredentialStatus, +} from "../gen/engram/mcp/v1/control_pb.ts"; -export type CredentialControlClient = Client; +import { SessionPolicyService } from "../gen/engram/mcp/v1/session_policy_pb.ts"; + +type CredentialWireClient = Client; +type StatusMethod = "putSecret" | "completeAuthorization" | "getStatus" | "revoke"; +export type CredentialControlClient = Omit & { + [K in StatusMethod]: (...args: Parameters) => Promise; +}; +/** Keep one status DTO at the lifecycle boundary; missing wire status is never ready. */ +export function unwrapCredentialControlClient(wire: CredentialWireClient): CredentialControlClient { + function status(response: { status?: CredentialStatus }): CredentialStatus { + if (!response.status) + throw new ConnectError("credential control status unavailable", Code.DataLoss); + return response.status; + } + return { + ...wire, + putSecret: async (...args) => status(await wire.putSecret(...args)), + completeAuthorization: async (...args) => status(await wire.completeAuthorization(...args)), + getStatus: async (...args) => status(await wire.getStatus(...args)), + revoke: async (...args) => status(await wire.revoke(...args)), + }; +} +export type SessionPolicyControlClient = Client; export interface CredentialControlTls { url: string; ca: string; @@ -13,6 +38,16 @@ export interface CredentialControlTls { } /** Dedicated transport: explicit trust roots, both certificate identities, no retries. */ export function makeCredentialControlClient(tls: CredentialControlTls): CredentialControlClient { + return unwrapCredentialControlClient( + createClient(CredentialControlService, makeControlTransport(tls, 32768)), + ); +} +export function makeSessionPolicyControlClient( + tls: CredentialControlTls, +): Client { + return createClient(SessionPolicyService, makeControlTransport(tls, 4 * 1024 * 1024)); +} +function makeControlTransport(tls: CredentialControlTls, maximumBytes: number) { const url = new URL(tls.url); if ( url.protocol !== "https:" || @@ -27,41 +62,38 @@ export function makeCredentialControlClient(tls: CredentialControlTls): Credenti !/^(?:[0-9A-F]{2}:){31}[0-9A-F]{2}$/.test(tls.serverFingerprint) ) throw new Error("MCP credential control requires pinned mTLS"); - return createClient( - CredentialControl, - createGrpcTransport({ - baseUrl: tls.url, - readMaxBytes: 32768, - writeMaxBytes: 32768, - defaultTimeoutMs: 30000, - idleConnectionTimeoutMs: 1000, - nodeOptions: { - ca: tls.ca, - cert: tls.cert, - key: tls.key, - minVersion: "TLSv1.3", - rejectUnauthorized: true, - checkServerIdentity(host, cert) { - return ( - checkServerIdentity(host, cert) ?? - (cert.fingerprint256 === tls.serverFingerprint - ? undefined - : new Error("MCP control certificate mismatch")) + return createGrpcTransport({ + baseUrl: tls.url, + readMaxBytes: maximumBytes, + writeMaxBytes: maximumBytes, + defaultTimeoutMs: 30000, + idleConnectionTimeoutMs: 1000, + nodeOptions: { + ca: tls.ca, + cert: tls.cert, + key: tls.key, + minVersion: "TLSv1.3", + rejectUnauthorized: true, + checkServerIdentity(host, cert) { + return ( + checkServerIdentity(host, cert) ?? + (cert.fingerprint256 === tls.serverFingerprint + ? undefined + : new Error("MCP control certificate mismatch")) + ); + }, + }, + interceptors: [ + (next) => async (request) => { + try { + return await next(request); + } catch (error) { + throw new ConnectError( + "MCP credential control unavailable", + error instanceof ConnectError ? error.code : Code.Unavailable, ); - }, + } }, - interceptors: [ - (next) => async (request) => { - try { - return await next(request); - } catch (error) { - throw new ConnectError( - "MCP credential control unavailable", - error instanceof ConnectError ? error.code : Code.Unavailable, - ); - } - }, - ], - }), - ); + ], + }); } diff --git a/orchestrator/src/mcp/session-policy.ts b/orchestrator/src/mcp/session-policy.ts new file mode 100644 index 000000000..97abad385 --- /dev/null +++ b/orchestrator/src/mcp/session-policy.ts @@ -0,0 +1,173 @@ +import { Code, ConnectError } from "@connectrpc/connect"; +import { eq } from "drizzle-orm"; +import { create, type MessageInitShape } from "@bufbuild/protobuf"; +import { getDb } from "../db/client.ts"; +import { task, taskSession, user } from "../db/schema.ts"; +import { getSessionFromHeaders } from "../auth/session.ts"; +import { isServiceAccountEmail } from "../rpc/api-key.ts"; +import type { GetSession } from "../rpc/require.ts"; +import type { UnaryOverride } from "../rpc/passthrough.ts"; +import { + CompleteToolCallResponseSchema, + GetMcpWritePolicyResponseSchema, + type CompleteToolCallRequest, + type GetMcpWritePolicyRequest, +} from "../gen/engram/app/v1/session_pb.ts"; +import type { + AcceptPlanExitRequestSchema, + ReadWritePolicyRequestSchema, + AcceptPlanExitResponseSchema, + ReadWritePolicyResponseSchema, +} from "../gen/engram/mcp/v1/session_policy_pb.ts"; + +export interface SessionPolicyControl { + acceptPlanExit?( + request: MessageInitShape, + ): Promise>; + readWritePolicy?( + request: MessageInitShape, + ): Promise>; +} +/** Fresh principal and owner lookup. Cached session roles and owner projections do not authorize. */ +export async function authorizePolicyActor( + sessionId: string, + actorId: string, +): Promise<{ id: string; isAdmin: boolean }> { + const [row] = await getDb() + .select({ + ownerId: task.createdByUserId, + role: user.role, + email: user.email, + banned: user.banned, + }) + .from(taskSession) + .innerJoin(task, eq(taskSession.taskId, task.id)) + .innerJoin(user, eq(user.id, actorId)) + .where(eq(taskSession.sessionId, sessionId)) + .limit(1); + if ( + !row || + row.banned || + isServiceAccountEmail(row.email) || + (row.role !== "admin" && row.ownerId !== actorId) + ) + throw new ConnectError("human session owner or administrator required", Code.PermissionDenied); + return { id: actorId, isAdmin: row.role === "admin" }; +} +/** Overrides run after ordinary permission and tool-output guards, never as preflights. */ +export function makeSessionPolicyOverrides(deps?: { + control?: SessionPolicyControl; + getSession?: GetSession; + authorize?: typeof authorizePolicyActor; +}): Record { + const getSession = deps?.getSession ?? getSessionFromHeaders; + const authorize = deps?.authorize ?? authorizePolicyActor; + return { + async "SessionService.CompleteToolCall"(request, ctx) { + const req = request as CompleteToolCallRequest; + if (!req.expectedMcpPlanReview) return undefined; + let result: unknown; + try { + result = JSON.parse(req.resultJson); + } catch { + throw new ConnectError("invalid plan result", Code.InvalidArgument); + } + if ( + !result || + typeof result !== "object" || + !("decision" in result) || + result.decision !== "approve" + ) { + // Generic rejection uses the existing delivery path and cannot change policy. + req.expectedMcpPlanReview = undefined; + return undefined; + } + const review = req.expectedMcpPlanReview; + if (review.policyRef?.sessionId !== req.sessionId || review.toolCallId !== req.toolCallId) + throw new ConnectError("plan review changed; reload the plan", Code.FailedPrecondition); + const session = await getSession(ctx.requestHeader); + if (!session) throw new ConnectError("unauthenticated", Code.Unauthenticated); + const actor = await authorize(req.sessionId, session.user.id); + const accept = deps?.control?.acceptPlanExit; + if (!accept) + throw new ConnectError("session policy control unavailable", Code.FailedPrecondition); + try { + await accept({ + protocol: 1, + expected: review, + actorId: actor.id, + resultJson: req.resultJson, + }); + } catch { + throw new ConnectError( + "session policy acceptance unavailable; reload the plan", + Code.FailedPrecondition, + ); + } + return { + response: create(CompleteToolCallResponseSchema, { + sessionId: req.sessionId, + note: "tool result queued", + }), + }; + }, + async "SessionService.GetMcpWritePolicy"(request, ctx) { + const req = request as GetMcpWritePolicyRequest; + const session = await getSession(ctx.requestHeader); + if (!session) throw new ConnectError("unauthenticated", Code.Unauthenticated); + await authorize(req.sessionId, session.user.id); + const read = deps?.control?.readWritePolicy; + if (!read) + throw new ConnectError("session policy control unavailable", Code.FailedPrecondition); + try { + const result = await read({ + protocol: 1, + sessionId: req.sessionId, + planToolCallId: req.planToolCallId, + }); + if (!result.state) throw new Error("policy state missing"); + const response = create(GetMcpWritePolicyResponseSchema, result.state); + response.allowedActions = response.planReview + ? ["approve_current_plan"] + : !response.policy?.initialized + ? ["start_supported_session"] + : response.policy.writeEligibility !== "write_eligible" && + response.policy.ancestorPolicyRefs.length + ? ["inspect_ancestor_policy"] + : []; + return { response }; + } catch { + throw new ConnectError("session policy read unavailable", Code.FailedPrecondition); + } + }, + }; +} + +export interface RequestPolicyContext { + sessionId: string; + runId: string; + toolCallId: string; + acceptedPolicyRef: import("../gen/engram/app/v1/session_pb.ts").McpWritePolicyRef; + acceptedWriteEligibility: string; + ancestorPolicyRefs: import("../gen/engram/app/v1/session_pb.ts").McpWritePolicyRef[]; +} +/** The journal persists the returned context before any authorization or dispatch. */ +export function makeSessionWritePolicy( + control: import("./control-client.ts").SessionPolicyControlClient, +) { + return { + async readForRequest(input: { sessionId: string }) { + const response = await control.readWritePolicy({ protocol: 1, sessionId: input.sessionId }); + if (!response.state?.policy?.ref || response.state.policy.ref.sessionId !== input.sessionId) + throw new ConnectError("session policy unavailable", Code.FailedPrecondition); + return response.state.policy; + }, + /** After the durable barrier, immediately before dispatch. Never cache a success. */ + async validateDispatchFence(context: RequestPolicyContext) { + const result = await control.validateDispatchFence({ protocol: 1, ...context }); + if (result.protocol !== 1 || !["current", "stale", "unknown"].includes(result.status)) + throw new ConnectError("session policy fence unavailable", Code.FailedPrecondition); + return result.status; + }, + }; +} diff --git a/orchestrator/src/rpc/passthrough.ts b/orchestrator/src/rpc/passthrough.ts index 9fcd04449..40031c16b 100644 --- a/orchestrator/src/rpc/passthrough.ts +++ b/orchestrator/src/rpc/passthrough.ts @@ -61,6 +61,8 @@ export type ResolveOwner = (sessionId: string) => Promise; /** Optional per-method pre-flight, keyed by policyKey ("ImageService.DisableImage"). * Runs AFTER the authz gate and BEFORE the upstream forward. Throws to block. */ +export type UnaryOverride = (req: unknown, ctx: HandlerContext) => Promise<{ response: unknown } | undefined>; + export type Preflight = (req: unknown, ctx: HandlerContext) => Promise; // --------------------------------------------------------------------------- @@ -177,6 +179,7 @@ export function registerPassthrough( getSession?: GetSession, resolveOwner?: ResolveOwner, preflight?: Record, + overrides?: Record, ): void { const resolveSession: GetSession = getSession ?? @@ -277,6 +280,8 @@ export function registerPassthrough( await gate(req, ctx); const pf = preflight?.[key]; if (pf) await pf(req, ctx); + const handled = await overrides?.[key]?.(req, ctx); + if (handled) return handled.response; let res; try { res = await upstream.unary( diff --git a/web/src/gen/engram/app/v1/session-SessionService_connectquery.ts b/web/src/gen/engram/app/v1/session-SessionService_connectquery.ts index e284286ab..b17996e0a 100644 --- a/web/src/gen/engram/app/v1/session-SessionService_connectquery.ts +++ b/web/src/gen/engram/app/v1/session-SessionService_connectquery.ts @@ -60,6 +60,11 @@ export const dequeueQueuedPrompt = SessionService.method.dequeueQueuedPrompt; */ export const completeToolCall = SessionService.method.completeToolCall; +/** + * @generated from rpc engram.app.v1.SessionService.GetMcpWritePolicy + */ +export const getMcpWritePolicy = SessionService.method.getMcpWritePolicy; + /** * ADR 0060: unary, paginated, UNFILTERED read of the persistent event log * (curation is the consumer's concern). The reverse-channel pump diff --git a/web/src/gen/engram/app/v1/session_pb.ts b/web/src/gen/engram/app/v1/session_pb.ts index 2b38adaa2..fd81b07da 100644 --- a/web/src/gen/engram/app/v1/session_pb.ts +++ b/web/src/gen/engram/app/v1/session_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/session.proto. */ export const file_engram_app_v1_session: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vYXBwL3YxL3Nlc3Npb24ucHJvdG8SDWVuZ3JhbS5hcHAudjEiFQoTTGlzdFNlc3Npb25zUmVxdWVzdCJIChRMaXN0U2Vzc2lvbnNSZXNwb25zZRIwCghzZXNzaW9ucxgBIAMoCzIeLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbkxpc3RJdGVtIrQBCg9TZXNzaW9uTGlzdEl0ZW0SJwoHc2Vzc2lvbhgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbhIYCgtvd25lcl9lbWFpbBgCIAEoCUgAiAEBEhcKCm93bmVyX25hbWUYAyABKAlIAYgBARIXCgpvd25lcl9raW5kGAQgASgJSAKIAQFCDgoMX293bmVyX2VtYWlsQg0KC19vd25lcl9uYW1lQg0KC19vd25lcl9raW5kIpgCCgdTZXNzaW9uEgoKAmlkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIUCgdob3N0X2lkGAMgASgJSACIAQESFwoKc2FuZGJveF9pZBgEIAEoCUgBiAEBEg0KBWltYWdlGAUgASgJEgwKBG1vZGUYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRIWCg5sYXN0X2FjdGl2ZV9hdBgIIAEoCRIcCg9zdWdnZXN0ZWRfdGl0bGUYCSABKAlIAogBARIaCg1sYXN0X2V2ZW50X2F0GAogASgJSAOIAQFCCgoIX2hvc3RfaWRCDQoLX3NhbmRib3hfaWRCEgoQX3N1Z2dlc3RlZF90aXRsZUIQCg5fbGFzdF9ldmVudF9hdCK4BQoUQ3JlYXRlU2Vzc2lvblJlcXVlc3QSEQoJaW1hZ2VfdXJpGAEgASgJEgwKBG1vZGUYAiABKAkSEwoGcHJvbXB0GAMgASgJSACIAQESFgoJcHJvbXB0X2lkGAcgASgJSAGIAQESQQoHc2VjcmV0cxgFIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlU2Vzc2lvblJlcXVlc3QuU2VjcmV0c0VudHJ5EkgKC2hhcm5lc3NfZW52GAYgAygLMjMuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdC5IYXJuZXNzRW52RW50cnkSFwoPc2VsZWN0ZWRfc2tpbGxzGAggAygJEhQKDGNhcGFiaWxpdGllcxgJIAMoCRIfChdpbnRlZ3JhdGlvbl9wb2xpY3lfanNvbhgKIAEoCRIUCgdoYXJuZXNzGAsgASgJSAKIAQESPwoQb2F1dGhfY3JlZGVudGlhbBgMIAEoCzIlLmVuZ3JhbS5hcHAudjEuT0F1dGhDcmVkZW50aWFsQmluZGluZxIZCgxoYXJuZXNzX21vZGUYDSABKAlIA4gBARIhChRyZXF1ZXN0ZWRfc2Vzc2lvbl9pZBgOIAEoCUgEiAEBEigKBGFwcHMYDyADKAsyGi5lbmdyYW0uYXBwLnYxLkFwcEVuZHBvaW50Gi4KDFNlY3JldHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjEKD0hhcm5lc3NFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgkKB19wcm9tcHRCDAoKX3Byb21wdF9pZEIKCghfaGFybmVzc0IPCg1faGFybmVzc19tb2RlQhcKFV9yZXF1ZXN0ZWRfc2Vzc2lvbl9pZEoECAQQBSItCgtBcHBFbmRwb2ludBIQCghob3N0bmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNIlgKFk9BdXRoQ3JlZGVudGlhbEJpbmRpbmcSLAoHc3ViamVjdBgBIAEoCzIbLmVuZ3JhbS5hcHAudjEuT0F1dGhTdWJqZWN0EhAKCHByb3ZpZGVyGAIgASgJImAKFUNyZWF0ZVNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIVCg1pbWFnZV92ZXJzaW9uGAMgASgJEgwKBGtpbmQYBCABKAkiJwoRR2V0U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSI9ChJHZXRTZXNzaW9uUmVzcG9uc2USJwoHc2Vzc2lvbhgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbiIqChREZWxldGVTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhcKFURlbGV0ZVNlc3Npb25SZXNwb25zZSJ0ChFTZW5kUHJvbXB0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEQoJcHJvbXB0X2lkGAMgASgJEhkKDGhhcm5lc3NfbW9kZRgEIAEoCUgAiAEBQg8KDV9oYXJuZXNzX21vZGUiNgoSU2VuZFByb21wdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSI2ChBJbnRlcnJ1cHRSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSDgoGc291cmNlGAIgASgJIjUKEUludGVycnVwdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSJOChdFZGl0UXVldWVkUHJvbXB0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhEKCXByb21wdF9pZBgCIAEoCRIMCgR0ZXh0GAMgASgJIjwKGEVkaXRRdWV1ZWRQcm9tcHRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEgwKBG5vdGUYAiABKAkiQwoaRGVxdWV1ZVF1ZXVlZFByb21wdFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIRCglwcm9tcHRfaWQYAiABKAkiPwobRGVxdWV1ZVF1ZXVlZFByb21wdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSJYChdDb21wbGV0ZVRvb2xDYWxsUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCRITCgtyZXN1bHRfanNvbhgDIAEoCSI8ChhDb21wbGV0ZVRvb2xDYWxsUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRub3RlGAIgASgJIl0KE1N0cmVhbUV2ZW50c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRISCgVzaW5jZRgCIAEoA0gAiAEBEhQKDGR1cmFibGVfb25seRgDIAEoCEIICgZfc2luY2UiTAoMU2Vzc2lvbkV2ZW50EhAKA2lkeBgBIAEoA0gAiAEBEgwKBGtpbmQYAiABKAkSFAoMcGF5bG9hZF9qc29uGAMgASgJQgYKBF9pZHgivQEKGExpc3RTZXNzaW9uRXZlbnRzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhYKCWFmdGVyX2lkeBgCIAEoA0gAiAEBEhIKBWxpbWl0GAMgASgDSAGIAQESFwoKYmVmb3JlX2lkeBgEIAEoA0gCiAEBEg0KBWtpbmRzGAUgAygJEhIKCnRvb2xfbmFtZXMYBiADKAlCDAoKX2FmdGVyX2lkeEIICgZfbGltaXRCDQoLX2JlZm9yZV9pZHgiYAoZTGlzdFNlc3Npb25FdmVudHNSZXNwb25zZRIrCgZldmVudHMYASADKAsyGy5lbmdyYW0uYXBwLnYxLlNlc3Npb25FdmVudBIWCg5uZXh0X2FmdGVyX2lkeBgCIAEoAyKXAwoLRXhlY1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgdjb21tYW5kGAIgASgJSACIAQESDAoEYXJndhgDIAMoCRIwCgNlbnYYBCADKAsyIy5lbmdyYW0uYXBwLnYxLkV4ZWNSZXF1ZXN0LkVudkVudHJ5EhQKB3dvcmtkaXIYBSABKAlIAYgBARIZCgx0aW1lb3V0X3NlY3MYBiABKARIAogBARIUCgdleGVjX2lkGAcgASgJSAOIAQESGgoNc3Rkb3V0X29mZnNldBgIIAEoBEgEiAEBEhoKDXN0ZGVycl9vZmZzZXQYCSABKARIBYgBARIRCgR3YWtlGAogASgISAaIAQEaKgoIRW52RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfY29tbWFuZEIKCghfd29ya2RpckIPCg1fdGltZW91dF9zZWNzQgoKCF9leGVjX2lkQhAKDl9zdGRvdXRfb2Zmc2V0QhAKDl9zdGRlcnJfb2Zmc2V0QgcKBV93YWtlIjgKEUNhbmNlbEV4ZWNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSDwoHZXhlY19pZBgCIAEoCSIUChJDYW5jZWxFeGVjUmVzcG9uc2UidQoRV3JpdGVGaWxlTWV0YWRhdGESEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJEhIKCnNpemVfYnl0ZXMYAyABKAQSDgoGc2hhMjU2GAQgASgJEhEKBG1vZGUYBSABKA1IAIgBAUIHCgVfbW9kZSJiChBXcml0ZUZpbGVSZXF1ZXN0EjQKCG1ldGFkYXRhGAEgASgLMiAuZW5ncmFtLmFwcC52MS5Xcml0ZUZpbGVNZXRhZGF0YUgAEg8KBWNodW5rGAIgASgMSABCBwoFZnJhbWUiRQoRV3JpdGVGaWxlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCSIzCg9SZWFkRmlsZVJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJIlcKEFJlYWRGaWxlTWV0YWRhdGESDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCRIRCglmaWxlX25hbWUYBCABKAkiYQoQUmVhZEZpbGVSZXNwb25zZRIzCghtZXRhZGF0YRgBIAEoCzIfLmVuZ3JhbS5hcHAudjEuUmVhZEZpbGVNZXRhZGF0YUgAEg8KBWNodW5rGAIgASgMSABCBwoFZnJhbWUiVwoQQ29weUZpbGVzUmVxdWVzdBIZChFzb3VyY2Vfc2Vzc2lvbl9pZBgBIAEoCRIZChF0YXJnZXRfc2Vzc2lvbl9pZBgCIAEoCRINCgVwYXRocxgDIAMoCSJCCg5Db3B5RmlsZVJlc3VsdBIMCgRwYXRoGAEgASgJEhIKCnNpemVfYnl0ZXMYAiABKAQSDgoGc2hhMjU2GAMgASgJIkEKEUNvcHlGaWxlc1Jlc3BvbnNlEiwKBWZpbGVzGAEgAygLMh0uZW5ncmFtLmFwcC52MS5Db3B5RmlsZVJlc3VsdCKRAQoKRXhlY091dHB1dBItCgdzdGFydGVkGAEgASgLMhouZW5ncmFtLmFwcC52MS5FeGVjU3RhcnRlZEgAEhAKBnN0ZG91dBgCIAEoDEgAEhAKBnN0ZGVychgDIAEoDEgAEicKBGV4aXQYBCABKAsyFy5lbmdyYW0uYXBwLnYxLkV4ZWNFeGl0SABCBwoFZXZlbnQiHgoLRXhlY1N0YXJ0ZWQSDwoHZXhlY19pZBgBIAEoCSJfCghFeGVjRXhpdBIYCgtleGl0X3N0YXR1cxgBIAEoBUgAiAEBEikKBnJ1c2FnZRgCIAEoCzIZLmVuZ3JhbS5hcHAudjEuRXhlY1J1c2FnZUIOCgxfZXhpdF9zdGF0dXMimQEKCkV4ZWNSdXNhZ2USDwoHd2FsbF9tcxgBIAEoBBIYCgtwZWFrX3Jzc19rYhgCIAEoBEgAiAEBEhgKC3VzZXJfY3B1X21zGAMgASgESAGIAQESFwoKc3lzX2NwdV9tcxgEIAEoBEgCiAEBQg4KDF9wZWFrX3Jzc19rYkIOCgxfdXNlcl9jcHVfbXNCDQoLX3N5c19jcHVfbXMieQoNR2V0TG9nUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhEKBGtpbmQYAiABKAlIAIgBARISCgVsaW1pdBgDIAEoA0gBiAEBEhEKBHRhaWwYBCABKAhIAogBAUIHCgVfa2luZEIICgZfbGltaXRCBwoFX3RhaWwiZAoOR2V0TG9nUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRraW5kGAIgASgJEjAKBmV2ZW50cxgDIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ29udmVyc2F0aW9uRW50cnkiUAoRQ29udmVyc2F0aW9uRW50cnkSCwoDaWR4GAEgASgDEgwKBGtpbmQYAiABKAkSCgoCYXQYAyABKAkSFAoMcGF5bG9hZF9qc29uGAQgASgJIiUKD1NuYXBzaG90UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIoYBChBTbmFwc2hvdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSGAoLc25hcHNob3RfaWQYAiABKAlIAIgBARIXCgpzaXplX2J5dGVzGAMgASgESAGIAQESDAoEbm90ZRgEIAEoCUIOCgxfc25hcHNob3RfaWRCDQoLX3NpemVfYnl0ZXMiKgoURW5zdXJlQnJvd3NlclJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIlChVFbnN1cmVCcm93c2VyUmVzcG9uc2USDAoEcG9ydBgBIAEoDSImChBFbnN1cmVJZGVSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiIQoRRW5zdXJlSWRlUmVzcG9uc2USDAoEcG9ydBgBIAEoDSIjCg1SZXN1bWVSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkihAEKDlJlc3VtZVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSGAoLc25hcHNob3RfaWQYAiABKAlIAIgBARIXCgpzaXplX2J5dGVzGAMgASgESAGIAQESDAoEbm90ZRgEIAEoCUIOCgxfc25hcHNob3RfaWRCDQoLX3NpemVfYnl0ZXMiJwoRRXZpY3RMb2NhbFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIUChJFdmljdExvY2FsUmVzcG9uc2UiJgoQRXZpY3RJZGxlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjcKEUV2aWN0SWRsZVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDgoGc3RhdHVzGAIgASgJIigKEkdldENvd1N0YXRlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJImQKE0dldENvd1N0YXRlUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIvCgVzdGF0ZRgCIAEoCzIbLmVuZ3JhbS5hcHAudjEuQ293U3RhdGVWaWV3SACIAQFCCAoGX3N0YXRlIroDCgxDb3dTdGF0ZVZpZXcSEgoKc2FuZGJveF9pZBgBIAEoCRIXCgpzZXNzaW9uX2lkGAIgASgJSACIAQESGAoQZGlza19tYW5pZmVzdF9pZBgDIAEoCRIdChVkaXNrX21hbmlmZXN0X3ZlcnNpb24YBCABKAQSFAoMZGlydHlfY2h1bmtzGAUgASgNEhMKC2RpcnR5X2J5dGVzGAYgASgEEhoKDWxhc3RfZmx1c2hfYXQYByABKAlIAYgBARITCgtiYXNlX2NodW5rcxgIIAEoDRIZChFiYXNlX2NodW5rc19sb2NhbBgJIAEoDRIfChJtZW1vcnlfbWFuaWZlc3RfaWQYCiABKAlIAogBARIkChdtZW1vcnlfbWFuaWZlc3RfdmVyc2lvbhgLIAEoBEgDiAEBEh0KEGxhc3Rfc25hcHNob3RfYXQYDCABKAlIBIgBAUINCgtfc2Vzc2lvbl9pZEIQCg5fbGFzdF9mbHVzaF9hdEIVChNfbWVtb3J5X21hbmlmZXN0X2lkQhoKGF9tZW1vcnlfbWFuaWZlc3RfdmVyc2lvbkITChFfbGFzdF9zbmFwc2hvdF9hdCIsChZMaXN0Q2hlY2twb2ludHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiZAoXTGlzdENoZWNrcG9pbnRzUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRI1CgtjaGVja3BvaW50cxgCIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ2hlY2twb2ludFN1bW1hcnkipgEKEUNoZWNrcG9pbnRTdW1tYXJ5EhMKC3NuYXBzaG90X2lkGAEgASgJEhIKCmNyZWF0ZWRfYXQYAiABKAkSEgoKc2l6ZV9ieXRlcxgDIAEoBBIaCg1ldmVudHNfY3Vyc29yGAQgASgDSACIAQESEwoLcmVjb3ZlcmFibGUYBSABKAgSEQoJaXNfbGF0ZXN0GAYgASgIQhAKDl9ldmVudHNfY3Vyc29yIj0KEkdldEFydGlmYWN0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhMKC2FydGlmYWN0X2lkGAIgASgJImIKE0dldEFydGlmYWN0UmVzcG9uc2USMwoIbWV0YWRhdGEYASABKAsyHy5lbmdyYW0uYXBwLnYxLkFydGlmYWN0TWV0YWRhdGFIABIPCgVjaHVuaxgCIAEoDEgAQgUKA21zZyJNChBBcnRpZmFjdE1ldGFkYXRhEhIKCm1lZGlhX3R5cGUYASABKAkSEgoKc2l6ZV9ieXRlcxgCIAEoBBIRCglmaWxlX25hbWUYAyABKAkiewodQ3JlYXRlQXJ0aWZhY3RGcm9tUGF0aFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJEhQKB2NhcHRpb24YAyABKAlIAIgBARIWCg5zdXBwcmVzc19ldmVudBgEIAEoCEIKCghfY2FwdGlvbiJdCh5DcmVhdGVBcnRpZmFjdEZyb21QYXRoUmVzcG9uc2USEwoLYXJ0aWZhY3RfaWQYASABKAkSEgoKbWVkaWFfdHlwZRgCIAEoCRISCgpzaXplX2J5dGVzGAMgASgEIrQBChFSZWxheVNoZWxsUmVxdWVzdBIoCgRvcGVuGAEgASgLMhguZW5ncmFtLmFwcC52MS5TaGVsbE9wZW5IABIOCgR0ZXh0GAIgASgJSAASEAoGYmluYXJ5GAMgASgMSAASDgoEcGluZxgEIAEoDEgAEg4KBHBvbmcYBSABKAxIABIqCgVjbG9zZRgGIAEoCzIZLmVuZ3JhbS5hcHAudjEuU2hlbGxDbG9zZUgAQgcKBWZyYW1lIosBChJSZWxheVNoZWxsUmVzcG9uc2USDgoEdGV4dBgBIAEoCUgAEhAKBmJpbmFyeRgCIAEoDEgAEg4KBHBpbmcYAyABKAxIABIOCgRwb25nGAQgASgMSAASKgoFY2xvc2UYBSABKAsyGS5lbmdyYW0uYXBwLnYxLlNoZWxsQ2xvc2VIAEIHCgVmcmFtZSIfCglTaGVsbE9wZW4SEgoKc2Vzc2lvbl9pZBgBIAEoCSIqCgpTaGVsbENsb3NlEgwKBGNvZGUYASABKA0SDgoGcmVhc29uGAIgASgJIn8KEFJlbGF5UG9ydFJlcXVlc3QSJwoEb3BlbhgBIAEoCzIXLmVuZ3JhbS5hcHAudjEuUG9ydE9wZW5IABIOCgRkYXRhGAIgASgMSAASKQoFY2xvc2UYAyABKAsyGC5lbmdyYW0uYXBwLnYxLlBvcnRDbG9zZUgAQgcKBWZyYW1lIlcKEVJlbGF5UG9ydFJlc3BvbnNlEg4KBGRhdGEYASABKAxIABIpCgVjbG9zZRgCIAEoCzIYLmVuZ3JhbS5hcHAudjEuUG9ydENsb3NlSABCBwoFZnJhbWUiLAoIUG9ydE9wZW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwb3J0GAIgASgNIgsKCVBvcnRDbG9zZTLAEgoOU2Vzc2lvblNlcnZpY2USVwoMTGlzdFNlc3Npb25zEiIuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbnNSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbnNSZXNwb25zZRJaCg1DcmVhdGVTZXNzaW9uEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlU2Vzc2lvblJlc3BvbnNlElEKCkdldFNlc3Npb24SIC5lbmdyYW0uYXBwLnYxLkdldFNlc3Npb25SZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uUmVzcG9uc2USWgoNRGVsZXRlU2Vzc2lvbhIjLmVuZ3JhbS5hcHAudjEuRGVsZXRlU2Vzc2lvblJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkRlbGV0ZVNlc3Npb25SZXNwb25zZRJRCgpTZW5kUHJvbXB0EiAuZW5ncmFtLmFwcC52MS5TZW5kUHJvbXB0UmVxdWVzdBohLmVuZ3JhbS5hcHAudjEuU2VuZFByb21wdFJlc3BvbnNlEk4KCUludGVycnVwdBIfLmVuZ3JhbS5hcHAudjEuSW50ZXJydXB0UmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuSW50ZXJydXB0UmVzcG9uc2USYwoQRWRpdFF1ZXVlZFByb21wdBImLmVuZ3JhbS5hcHAudjEuRWRpdFF1ZXVlZFByb21wdFJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkVkaXRRdWV1ZWRQcm9tcHRSZXNwb25zZRJsChNEZXF1ZXVlUXVldWVkUHJvbXB0EikuZW5ncmFtLmFwcC52MS5EZXF1ZXVlUXVldWVkUHJvbXB0UmVxdWVzdBoqLmVuZ3JhbS5hcHAudjEuRGVxdWV1ZVF1ZXVlZFByb21wdFJlc3BvbnNlEmMKEENvbXBsZXRlVG9vbENhbGwSJi5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlVG9vbENhbGxSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5Db21wbGV0ZVRvb2xDYWxsUmVzcG9uc2USUQoMU3RyZWFtRXZlbnRzEiIuZW5ncmFtLmFwcC52MS5TdHJlYW1FdmVudHNSZXF1ZXN0GhsuZW5ncmFtLmFwcC52MS5TZXNzaW9uRXZlbnQwARJmChFMaXN0U2Vzc2lvbkV2ZW50cxInLmVuZ3JhbS5hcHAudjEuTGlzdFNlc3Npb25FdmVudHNSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbkV2ZW50c1Jlc3BvbnNlEj8KBEV4ZWMSGi5lbmdyYW0uYXBwLnYxLkV4ZWNSZXF1ZXN0GhkuZW5ncmFtLmFwcC52MS5FeGVjT3V0cHV0MAESUQoKQ2FuY2VsRXhlYxIgLmVuZ3JhbS5hcHAudjEuQ2FuY2VsRXhlY1JlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkNhbmNlbEV4ZWNSZXNwb25zZRJQCglXcml0ZUZpbGUSHy5lbmdyYW0uYXBwLnYxLldyaXRlRmlsZVJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLldyaXRlRmlsZVJlc3BvbnNlKAESTQoIUmVhZEZpbGUSHi5lbmdyYW0uYXBwLnYxLlJlYWRGaWxlUmVxdWVzdBofLmVuZ3JhbS5hcHAudjEuUmVhZEZpbGVSZXNwb25zZTABEk4KCUNvcHlGaWxlcxIfLmVuZ3JhbS5hcHAudjEuQ29weUZpbGVzUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuQ29weUZpbGVzUmVzcG9uc2USRQoGR2V0TG9nEhwuZW5ncmFtLmFwcC52MS5HZXRMb2dSZXF1ZXN0Gh0uZW5ncmFtLmFwcC52MS5HZXRMb2dSZXNwb25zZRJLCghTbmFwc2hvdBIeLmVuZ3JhbS5hcHAudjEuU25hcHNob3RSZXF1ZXN0Gh8uZW5ncmFtLmFwcC52MS5TbmFwc2hvdFJlc3BvbnNlEloKDUVuc3VyZUJyb3dzZXISIy5lbmdyYW0uYXBwLnYxLkVuc3VyZUJyb3dzZXJSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5FbnN1cmVCcm93c2VyUmVzcG9uc2USTgoJRW5zdXJlSWRlEh8uZW5ncmFtLmFwcC52MS5FbnN1cmVJZGVSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5FbnN1cmVJZGVSZXNwb25zZRJFCgZSZXN1bWUSHC5lbmdyYW0uYXBwLnYxLlJlc3VtZVJlcXVlc3QaHS5lbmdyYW0uYXBwLnYxLlJlc3VtZVJlc3BvbnNlElEKCkV2aWN0TG9jYWwSIC5lbmdyYW0uYXBwLnYxLkV2aWN0TG9jYWxSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5FdmljdExvY2FsUmVzcG9uc2USTgoJRXZpY3RJZGxlEh8uZW5ncmFtLmFwcC52MS5FdmljdElkbGVSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5FdmljdElkbGVSZXNwb25zZRJUCgtHZXRDb3dTdGF0ZRIhLmVuZ3JhbS5hcHAudjEuR2V0Q293U3RhdGVSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRDb3dTdGF0ZVJlc3BvbnNlEmAKD0xpc3RDaGVja3BvaW50cxIlLmVuZ3JhbS5hcHAudjEuTGlzdENoZWNrcG9pbnRzUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuTGlzdENoZWNrcG9pbnRzUmVzcG9uc2USVgoLR2V0QXJ0aWZhY3QSIS5lbmdyYW0uYXBwLnYxLkdldEFydGlmYWN0UmVxdWVzdBoiLmVuZ3JhbS5hcHAudjEuR2V0QXJ0aWZhY3RSZXNwb25zZTABEnUKFkNyZWF0ZUFydGlmYWN0RnJvbVBhdGgSLC5lbmdyYW0uYXBwLnYxLkNyZWF0ZUFydGlmYWN0RnJvbVBhdGhSZXF1ZXN0Gi0uZW5ncmFtLmFwcC52MS5DcmVhdGVBcnRpZmFjdEZyb21QYXRoUmVzcG9uc2UyZQoRU2hlbGxSZWxheVNlcnZpY2USUAoFUmVsYXkSIC5lbmdyYW0uYXBwLnYxLlJlbGF5U2hlbGxSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5SZWxheVNoZWxsUmVzcG9uc2UoATABMmIKEFBvcnRSZWxheVNlcnZpY2USTgoFUmVsYXkSHy5lbmdyYW0uYXBwLnYxLlJlbGF5UG9ydFJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLlJlbGF5UG9ydFJlc3BvbnNlKAEwAWIGcHJvdG8z", [file_engram_app_v1_oauth]); + fileDesc("ChtlbmdyYW0vYXBwL3YxL3Nlc3Npb24ucHJvdG8SDWVuZ3JhbS5hcHAudjEiFQoTTGlzdFNlc3Npb25zUmVxdWVzdCJIChRMaXN0U2Vzc2lvbnNSZXNwb25zZRIwCghzZXNzaW9ucxgBIAMoCzIeLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbkxpc3RJdGVtIrQBCg9TZXNzaW9uTGlzdEl0ZW0SJwoHc2Vzc2lvbhgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbhIYCgtvd25lcl9lbWFpbBgCIAEoCUgAiAEBEhcKCm93bmVyX25hbWUYAyABKAlIAYgBARIXCgpvd25lcl9raW5kGAQgASgJSAKIAQFCDgoMX293bmVyX2VtYWlsQg0KC19vd25lcl9uYW1lQg0KC19vd25lcl9raW5kIpgCCgdTZXNzaW9uEgoKAmlkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIUCgdob3N0X2lkGAMgASgJSACIAQESFwoKc2FuZGJveF9pZBgEIAEoCUgBiAEBEg0KBWltYWdlGAUgASgJEgwKBG1vZGUYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRIWCg5sYXN0X2FjdGl2ZV9hdBgIIAEoCRIcCg9zdWdnZXN0ZWRfdGl0bGUYCSABKAlIAogBARIaCg1sYXN0X2V2ZW50X2F0GAogASgJSAOIAQFCCgoIX2hvc3RfaWRCDQoLX3NhbmRib3hfaWRCEgoQX3N1Z2dlc3RlZF90aXRsZUIQCg5fbGFzdF9ldmVudF9hdCK4BQoUQ3JlYXRlU2Vzc2lvblJlcXVlc3QSEQoJaW1hZ2VfdXJpGAEgASgJEgwKBG1vZGUYAiABKAkSEwoGcHJvbXB0GAMgASgJSACIAQESFgoJcHJvbXB0X2lkGAcgASgJSAGIAQESQQoHc2VjcmV0cxgFIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlU2Vzc2lvblJlcXVlc3QuU2VjcmV0c0VudHJ5EkgKC2hhcm5lc3NfZW52GAYgAygLMjMuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdC5IYXJuZXNzRW52RW50cnkSFwoPc2VsZWN0ZWRfc2tpbGxzGAggAygJEhQKDGNhcGFiaWxpdGllcxgJIAMoCRIfChdpbnRlZ3JhdGlvbl9wb2xpY3lfanNvbhgKIAEoCRIUCgdoYXJuZXNzGAsgASgJSAKIAQESPwoQb2F1dGhfY3JlZGVudGlhbBgMIAEoCzIlLmVuZ3JhbS5hcHAudjEuT0F1dGhDcmVkZW50aWFsQmluZGluZxIZCgxoYXJuZXNzX21vZGUYDSABKAlIA4gBARIhChRyZXF1ZXN0ZWRfc2Vzc2lvbl9pZBgOIAEoCUgEiAEBEigKBGFwcHMYDyADKAsyGi5lbmdyYW0uYXBwLnYxLkFwcEVuZHBvaW50Gi4KDFNlY3JldHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjEKD0hhcm5lc3NFbnZFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgkKB19wcm9tcHRCDAoKX3Byb21wdF9pZEIKCghfaGFybmVzc0IPCg1faGFybmVzc19tb2RlQhcKFV9yZXF1ZXN0ZWRfc2Vzc2lvbl9pZEoECAQQBSItCgtBcHBFbmRwb2ludBIQCghob3N0bmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNIlgKFk9BdXRoQ3JlZGVudGlhbEJpbmRpbmcSLAoHc3ViamVjdBgBIAEoCzIbLmVuZ3JhbS5hcHAudjEuT0F1dGhTdWJqZWN0EhAKCHByb3ZpZGVyGAIgASgJImAKFUNyZWF0ZVNlc3Npb25SZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnN0YXR1cxgCIAEoCRIVCg1pbWFnZV92ZXJzaW9uGAMgASgJEgwKBGtpbmQYBCABKAkiJwoRR2V0U2Vzc2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSI9ChJHZXRTZXNzaW9uUmVzcG9uc2USJwoHc2Vzc2lvbhgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuU2Vzc2lvbiIqChREZWxldGVTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIhcKFURlbGV0ZVNlc3Npb25SZXNwb25zZSJ0ChFTZW5kUHJvbXB0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEgwKBHRleHQYAiABKAkSEQoJcHJvbXB0X2lkGAMgASgJEhkKDGhhcm5lc3NfbW9kZRgEIAEoCUgAiAEBQg8KDV9oYXJuZXNzX21vZGUiNgoSU2VuZFByb21wdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSI2ChBJbnRlcnJ1cHRSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSDgoGc291cmNlGAIgASgJIjUKEUludGVycnVwdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSJOChdFZGl0UXVldWVkUHJvbXB0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhEKCXByb21wdF9pZBgCIAEoCRIMCgR0ZXh0GAMgASgJIjwKGEVkaXRRdWV1ZWRQcm9tcHRSZXNwb25zZRISCgpzZXNzaW9uX2lkGAEgASgJEgwKBG5vdGUYAiABKAkiQwoaRGVxdWV1ZVF1ZXVlZFByb21wdFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIRCglwcm9tcHRfaWQYAiABKAkiPwobRGVxdWV1ZVF1ZXVlZFByb21wdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDAoEbm90ZRgCIAEoCSJXChFNY3BXcml0ZVBvbGljeVJlZhISCgpzZXNzaW9uX2lkGAEgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgCIAEoAxIVCg1iaW5kaW5nX2Vwb2NoGAMgASgDIscDChVNY3BUcnVzdGVkV3JpdGVQb2xpY3kSLQoDcmVmGAEgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIZChF3cml0ZV9lbGlnaWJpbGl0eRgCIAEoCRINCgVjYXVzZRgDIAEoCRIZChFhY2NlcHRlZF9pbnB1dF9pZBgEIAEoCRIjChZhdXRoZW50aWNhdGVkX2FjdG9yX2lkGAUgASgJSACIAQESFgoJcHJvbXB0X2lkGAYgASgJSAGIAQESHgoRcGxhbl90b29sX2NhbGxfaWQYByABKAlIAogBARIgChNwbGFuX3JlcXVlc3RfZGlnZXN0GAggASgJSAOIAQESPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYCSADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEg8KB2N1cnJlbnQYCiABKAgSEwoLaW5pdGlhbGl6ZWQYCyABKAhCGQoXX2F1dGhlbnRpY2F0ZWRfYWN0b3JfaWRCDAoKX3Byb21wdF9pZEIUChJfcGxhbl90b29sX2NhbGxfaWRCFgoUX3BsYW5fcmVxdWVzdF9kaWdlc3QiqQEKDU1jcFBsYW5SZXZpZXcSNAoKcG9saWN5X3JlZhgBIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFAoMdG9vbF9jYWxsX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIRCglldmVudF9pZHgYBCABKAMSFgoOcmVxdWVzdF9kaWdlc3QYBSABKAkSEQoJYXJnc19qc29uGAYgASgJImQKGEdldE1jcFdyaXRlUG9saWN5UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh4KEXBsYW5fdG9vbF9jYWxsX2lkGAIgASgJSACIAQFCFAoSX3BsYW5fdG9vbF9jYWxsX2lkIq0BChlHZXRNY3BXcml0ZVBvbGljeVJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5EjEKC3BsYW5fcmV2aWV3GAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3Eg4KBnJlYXNvbhgDIAEoCRIXCg9hbGxvd2VkX2FjdGlvbnMYBCADKAkimAEKF0NvbXBsZXRlVG9vbENhbGxSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJEhMKC3Jlc3VsdF9qc29uGAMgASgJEj4KGGV4cGVjdGVkX21jcF9wbGFuX3JldmlldxgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuTWNwUGxhblJldmlldyI8ChhDb21wbGV0ZVRvb2xDYWxsUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRub3RlGAIgASgJIl0KE1N0cmVhbUV2ZW50c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRISCgVzaW5jZRgCIAEoA0gAiAEBEhQKDGR1cmFibGVfb25seRgDIAEoCEIICgZfc2luY2UiTAoMU2Vzc2lvbkV2ZW50EhAKA2lkeBgBIAEoA0gAiAEBEgwKBGtpbmQYAiABKAkSFAoMcGF5bG9hZF9qc29uGAMgASgJQgYKBF9pZHgivQEKGExpc3RTZXNzaW9uRXZlbnRzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhYKCWFmdGVyX2lkeBgCIAEoA0gAiAEBEhIKBWxpbWl0GAMgASgDSAGIAQESFwoKYmVmb3JlX2lkeBgEIAEoA0gCiAEBEg0KBWtpbmRzGAUgAygJEhIKCnRvb2xfbmFtZXMYBiADKAlCDAoKX2FmdGVyX2lkeEIICgZfbGltaXRCDQoLX2JlZm9yZV9pZHgiYAoZTGlzdFNlc3Npb25FdmVudHNSZXNwb25zZRIrCgZldmVudHMYASADKAsyGy5lbmdyYW0uYXBwLnYxLlNlc3Npb25FdmVudBIWCg5uZXh0X2FmdGVyX2lkeBgCIAEoAyKXAwoLRXhlY1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIUCgdjb21tYW5kGAIgASgJSACIAQESDAoEYXJndhgDIAMoCRIwCgNlbnYYBCADKAsyIy5lbmdyYW0uYXBwLnYxLkV4ZWNSZXF1ZXN0LkVudkVudHJ5EhQKB3dvcmtkaXIYBSABKAlIAYgBARIZCgx0aW1lb3V0X3NlY3MYBiABKARIAogBARIUCgdleGVjX2lkGAcgASgJSAOIAQESGgoNc3Rkb3V0X29mZnNldBgIIAEoBEgEiAEBEhoKDXN0ZGVycl9vZmZzZXQYCSABKARIBYgBARIRCgR3YWtlGAogASgISAaIAQEaKgoIRW52RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfY29tbWFuZEIKCghfd29ya2RpckIPCg1fdGltZW91dF9zZWNzQgoKCF9leGVjX2lkQhAKDl9zdGRvdXRfb2Zmc2V0QhAKDl9zdGRlcnJfb2Zmc2V0QgcKBV93YWtlIjgKEUNhbmNlbEV4ZWNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSDwoHZXhlY19pZBgCIAEoCSIUChJDYW5jZWxFeGVjUmVzcG9uc2UidQoRV3JpdGVGaWxlTWV0YWRhdGESEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJEhIKCnNpemVfYnl0ZXMYAyABKAQSDgoGc2hhMjU2GAQgASgJEhEKBG1vZGUYBSABKA1IAIgBAUIHCgVfbW9kZSJiChBXcml0ZUZpbGVSZXF1ZXN0EjQKCG1ldGFkYXRhGAEgASgLMiAuZW5ncmFtLmFwcC52MS5Xcml0ZUZpbGVNZXRhZGF0YUgAEg8KBWNodW5rGAIgASgMSABCBwoFZnJhbWUiRQoRV3JpdGVGaWxlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCSIzCg9SZWFkRmlsZVJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJIlcKEFJlYWRGaWxlTWV0YWRhdGESDAoEcGF0aBgBIAEoCRISCgpzaXplX2J5dGVzGAIgASgEEg4KBnNoYTI1NhgDIAEoCRIRCglmaWxlX25hbWUYBCABKAkiYQoQUmVhZEZpbGVSZXNwb25zZRIzCghtZXRhZGF0YRgBIAEoCzIfLmVuZ3JhbS5hcHAudjEuUmVhZEZpbGVNZXRhZGF0YUgAEg8KBWNodW5rGAIgASgMSABCBwoFZnJhbWUiVwoQQ29weUZpbGVzUmVxdWVzdBIZChFzb3VyY2Vfc2Vzc2lvbl9pZBgBIAEoCRIZChF0YXJnZXRfc2Vzc2lvbl9pZBgCIAEoCRINCgVwYXRocxgDIAMoCSJCCg5Db3B5RmlsZVJlc3VsdBIMCgRwYXRoGAEgASgJEhIKCnNpemVfYnl0ZXMYAiABKAQSDgoGc2hhMjU2GAMgASgJIkEKEUNvcHlGaWxlc1Jlc3BvbnNlEiwKBWZpbGVzGAEgAygLMh0uZW5ncmFtLmFwcC52MS5Db3B5RmlsZVJlc3VsdCKRAQoKRXhlY091dHB1dBItCgdzdGFydGVkGAEgASgLMhouZW5ncmFtLmFwcC52MS5FeGVjU3RhcnRlZEgAEhAKBnN0ZG91dBgCIAEoDEgAEhAKBnN0ZGVychgDIAEoDEgAEicKBGV4aXQYBCABKAsyFy5lbmdyYW0uYXBwLnYxLkV4ZWNFeGl0SABCBwoFZXZlbnQiHgoLRXhlY1N0YXJ0ZWQSDwoHZXhlY19pZBgBIAEoCSJfCghFeGVjRXhpdBIYCgtleGl0X3N0YXR1cxgBIAEoBUgAiAEBEikKBnJ1c2FnZRgCIAEoCzIZLmVuZ3JhbS5hcHAudjEuRXhlY1J1c2FnZUIOCgxfZXhpdF9zdGF0dXMimQEKCkV4ZWNSdXNhZ2USDwoHd2FsbF9tcxgBIAEoBBIYCgtwZWFrX3Jzc19rYhgCIAEoBEgAiAEBEhgKC3VzZXJfY3B1X21zGAMgASgESAGIAQESFwoKc3lzX2NwdV9tcxgEIAEoBEgCiAEBQg4KDF9wZWFrX3Jzc19rYkIOCgxfdXNlcl9jcHVfbXNCDQoLX3N5c19jcHVfbXMieQoNR2V0TG9nUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhEKBGtpbmQYAiABKAlIAIgBARISCgVsaW1pdBgDIAEoA0gBiAEBEhEKBHRhaWwYBCABKAhIAogBAUIHCgVfa2luZEIICgZfbGltaXRCBwoFX3RhaWwiZAoOR2V0TG9nUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRraW5kGAIgASgJEjAKBmV2ZW50cxgDIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ29udmVyc2F0aW9uRW50cnkiUAoRQ29udmVyc2F0aW9uRW50cnkSCwoDaWR4GAEgASgDEgwKBGtpbmQYAiABKAkSCgoCYXQYAyABKAkSFAoMcGF5bG9hZF9qc29uGAQgASgJIiUKD1NuYXBzaG90UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIoYBChBTbmFwc2hvdFJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSGAoLc25hcHNob3RfaWQYAiABKAlIAIgBARIXCgpzaXplX2J5dGVzGAMgASgESAGIAQESDAoEbm90ZRgEIAEoCUIOCgxfc25hcHNob3RfaWRCDQoLX3NpemVfYnl0ZXMiKgoURW5zdXJlQnJvd3NlclJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIlChVFbnN1cmVCcm93c2VyUmVzcG9uc2USDAoEcG9ydBgBIAEoDSImChBFbnN1cmVJZGVSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiIQoRRW5zdXJlSWRlUmVzcG9uc2USDAoEcG9ydBgBIAEoDSIjCg1SZXN1bWVSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkihAEKDlJlc3VtZVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSGAoLc25hcHNob3RfaWQYAiABKAlIAIgBARIXCgpzaXplX2J5dGVzGAMgASgESAGIAQESDAoEbm90ZRgEIAEoCUIOCgxfc25hcHNob3RfaWRCDQoLX3NpemVfYnl0ZXMiJwoRRXZpY3RMb2NhbFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSIUChJFdmljdExvY2FsUmVzcG9uc2UiJgoQRXZpY3RJZGxlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJIjcKEUV2aWN0SWRsZVJlc3BvbnNlEhIKCnNlc3Npb25faWQYASABKAkSDgoGc3RhdHVzGAIgASgJIigKEkdldENvd1N0YXRlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJImQKE0dldENvd1N0YXRlUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRIvCgVzdGF0ZRgCIAEoCzIbLmVuZ3JhbS5hcHAudjEuQ293U3RhdGVWaWV3SACIAQFCCAoGX3N0YXRlIroDCgxDb3dTdGF0ZVZpZXcSEgoKc2FuZGJveF9pZBgBIAEoCRIXCgpzZXNzaW9uX2lkGAIgASgJSACIAQESGAoQZGlza19tYW5pZmVzdF9pZBgDIAEoCRIdChVkaXNrX21hbmlmZXN0X3ZlcnNpb24YBCABKAQSFAoMZGlydHlfY2h1bmtzGAUgASgNEhMKC2RpcnR5X2J5dGVzGAYgASgEEhoKDWxhc3RfZmx1c2hfYXQYByABKAlIAYgBARITCgtiYXNlX2NodW5rcxgIIAEoDRIZChFiYXNlX2NodW5rc19sb2NhbBgJIAEoDRIfChJtZW1vcnlfbWFuaWZlc3RfaWQYCiABKAlIAogBARIkChdtZW1vcnlfbWFuaWZlc3RfdmVyc2lvbhgLIAEoBEgDiAEBEh0KEGxhc3Rfc25hcHNob3RfYXQYDCABKAlIBIgBAUINCgtfc2Vzc2lvbl9pZEIQCg5fbGFzdF9mbHVzaF9hdEIVChNfbWVtb3J5X21hbmlmZXN0X2lkQhoKGF9tZW1vcnlfbWFuaWZlc3RfdmVyc2lvbkITChFfbGFzdF9zbmFwc2hvdF9hdCIsChZMaXN0Q2hlY2twb2ludHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkiZAoXTGlzdENoZWNrcG9pbnRzUmVzcG9uc2USEgoKc2Vzc2lvbl9pZBgBIAEoCRI1CgtjaGVja3BvaW50cxgCIAMoCzIgLmVuZ3JhbS5hcHAudjEuQ2hlY2twb2ludFN1bW1hcnkipgEKEUNoZWNrcG9pbnRTdW1tYXJ5EhMKC3NuYXBzaG90X2lkGAEgASgJEhIKCmNyZWF0ZWRfYXQYAiABKAkSEgoKc2l6ZV9ieXRlcxgDIAEoBBIaCg1ldmVudHNfY3Vyc29yGAQgASgDSACIAQESEwoLcmVjb3ZlcmFibGUYBSABKAgSEQoJaXNfbGF0ZXN0GAYgASgIQhAKDl9ldmVudHNfY3Vyc29yIj0KEkdldEFydGlmYWN0UmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhMKC2FydGlmYWN0X2lkGAIgASgJImIKE0dldEFydGlmYWN0UmVzcG9uc2USMwoIbWV0YWRhdGEYASABKAsyHy5lbmdyYW0uYXBwLnYxLkFydGlmYWN0TWV0YWRhdGFIABIPCgVjaHVuaxgCIAEoDEgAQgUKA21zZyJNChBBcnRpZmFjdE1ldGFkYXRhEhIKCm1lZGlhX3R5cGUYASABKAkSEgoKc2l6ZV9ieXRlcxgCIAEoBBIRCglmaWxlX25hbWUYAyABKAkiewodQ3JlYXRlQXJ0aWZhY3RGcm9tUGF0aFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwYXRoGAIgASgJEhQKB2NhcHRpb24YAyABKAlIAIgBARIWCg5zdXBwcmVzc19ldmVudBgEIAEoCEIKCghfY2FwdGlvbiJdCh5DcmVhdGVBcnRpZmFjdEZyb21QYXRoUmVzcG9uc2USEwoLYXJ0aWZhY3RfaWQYASABKAkSEgoKbWVkaWFfdHlwZRgCIAEoCRISCgpzaXplX2J5dGVzGAMgASgEIrQBChFSZWxheVNoZWxsUmVxdWVzdBIoCgRvcGVuGAEgASgLMhguZW5ncmFtLmFwcC52MS5TaGVsbE9wZW5IABIOCgR0ZXh0GAIgASgJSAASEAoGYmluYXJ5GAMgASgMSAASDgoEcGluZxgEIAEoDEgAEg4KBHBvbmcYBSABKAxIABIqCgVjbG9zZRgGIAEoCzIZLmVuZ3JhbS5hcHAudjEuU2hlbGxDbG9zZUgAQgcKBWZyYW1lIosBChJSZWxheVNoZWxsUmVzcG9uc2USDgoEdGV4dBgBIAEoCUgAEhAKBmJpbmFyeRgCIAEoDEgAEg4KBHBpbmcYAyABKAxIABIOCgRwb25nGAQgASgMSAASKgoFY2xvc2UYBSABKAsyGS5lbmdyYW0uYXBwLnYxLlNoZWxsQ2xvc2VIAEIHCgVmcmFtZSIfCglTaGVsbE9wZW4SEgoKc2Vzc2lvbl9pZBgBIAEoCSIqCgpTaGVsbENsb3NlEgwKBGNvZGUYASABKA0SDgoGcmVhc29uGAIgASgJIn8KEFJlbGF5UG9ydFJlcXVlc3QSJwoEb3BlbhgBIAEoCzIXLmVuZ3JhbS5hcHAudjEuUG9ydE9wZW5IABIOCgRkYXRhGAIgASgMSAASKQoFY2xvc2UYAyABKAsyGC5lbmdyYW0uYXBwLnYxLlBvcnRDbG9zZUgAQgcKBWZyYW1lIlcKEVJlbGF5UG9ydFJlc3BvbnNlEg4KBGRhdGEYASABKAxIABIpCgVjbG9zZRgCIAEoCzIYLmVuZ3JhbS5hcHAudjEuUG9ydENsb3NlSABCBwoFZnJhbWUiLAoIUG9ydE9wZW4SEgoKc2Vzc2lvbl9pZBgBIAEoCRIMCgRwb3J0GAIgASgNIgsKCVBvcnRDbG9zZTKoEwoOU2Vzc2lvblNlcnZpY2USVwoMTGlzdFNlc3Npb25zEiIuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbnNSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbnNSZXNwb25zZRJaCg1DcmVhdGVTZXNzaW9uEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlU2Vzc2lvblJlc3BvbnNlElEKCkdldFNlc3Npb24SIC5lbmdyYW0uYXBwLnYxLkdldFNlc3Npb25SZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uUmVzcG9uc2USWgoNRGVsZXRlU2Vzc2lvbhIjLmVuZ3JhbS5hcHAudjEuRGVsZXRlU2Vzc2lvblJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkRlbGV0ZVNlc3Npb25SZXNwb25zZRJRCgpTZW5kUHJvbXB0EiAuZW5ncmFtLmFwcC52MS5TZW5kUHJvbXB0UmVxdWVzdBohLmVuZ3JhbS5hcHAudjEuU2VuZFByb21wdFJlc3BvbnNlEk4KCUludGVycnVwdBIfLmVuZ3JhbS5hcHAudjEuSW50ZXJydXB0UmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuSW50ZXJydXB0UmVzcG9uc2USYwoQRWRpdFF1ZXVlZFByb21wdBImLmVuZ3JhbS5hcHAudjEuRWRpdFF1ZXVlZFByb21wdFJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkVkaXRRdWV1ZWRQcm9tcHRSZXNwb25zZRJsChNEZXF1ZXVlUXVldWVkUHJvbXB0EikuZW5ncmFtLmFwcC52MS5EZXF1ZXVlUXVldWVkUHJvbXB0UmVxdWVzdBoqLmVuZ3JhbS5hcHAudjEuRGVxdWV1ZVF1ZXVlZFByb21wdFJlc3BvbnNlEmMKEENvbXBsZXRlVG9vbENhbGwSJi5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlVG9vbENhbGxSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5Db21wbGV0ZVRvb2xDYWxsUmVzcG9uc2USZgoRR2V0TWNwV3JpdGVQb2xpY3kSJy5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZRJRCgxTdHJlYW1FdmVudHMSIi5lbmdyYW0uYXBwLnYxLlN0cmVhbUV2ZW50c1JlcXVlc3QaGy5lbmdyYW0uYXBwLnYxLlNlc3Npb25FdmVudDABEmYKEUxpc3RTZXNzaW9uRXZlbnRzEicuZW5ncmFtLmFwcC52MS5MaXN0U2Vzc2lvbkV2ZW50c1JlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLkxpc3RTZXNzaW9uRXZlbnRzUmVzcG9uc2USPwoERXhlYxIaLmVuZ3JhbS5hcHAudjEuRXhlY1JlcXVlc3QaGS5lbmdyYW0uYXBwLnYxLkV4ZWNPdXRwdXQwARJRCgpDYW5jZWxFeGVjEiAuZW5ncmFtLmFwcC52MS5DYW5jZWxFeGVjUmVxdWVzdBohLmVuZ3JhbS5hcHAudjEuQ2FuY2VsRXhlY1Jlc3BvbnNlElAKCVdyaXRlRmlsZRIfLmVuZ3JhbS5hcHAudjEuV3JpdGVGaWxlUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuV3JpdGVGaWxlUmVzcG9uc2UoARJNCghSZWFkRmlsZRIeLmVuZ3JhbS5hcHAudjEuUmVhZEZpbGVSZXF1ZXN0Gh8uZW5ncmFtLmFwcC52MS5SZWFkRmlsZVJlc3BvbnNlMAESTgoJQ29weUZpbGVzEh8uZW5ncmFtLmFwcC52MS5Db3B5RmlsZXNSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5Db3B5RmlsZXNSZXNwb25zZRJFCgZHZXRMb2cSHC5lbmdyYW0uYXBwLnYxLkdldExvZ1JlcXVlc3QaHS5lbmdyYW0uYXBwLnYxLkdldExvZ1Jlc3BvbnNlEksKCFNuYXBzaG90Eh4uZW5ncmFtLmFwcC52MS5TbmFwc2hvdFJlcXVlc3QaHy5lbmdyYW0uYXBwLnYxLlNuYXBzaG90UmVzcG9uc2USWgoNRW5zdXJlQnJvd3NlchIjLmVuZ3JhbS5hcHAudjEuRW5zdXJlQnJvd3NlclJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkVuc3VyZUJyb3dzZXJSZXNwb25zZRJOCglFbnN1cmVJZGUSHy5lbmdyYW0uYXBwLnYxLkVuc3VyZUlkZVJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLkVuc3VyZUlkZVJlc3BvbnNlEkUKBlJlc3VtZRIcLmVuZ3JhbS5hcHAudjEuUmVzdW1lUmVxdWVzdBodLmVuZ3JhbS5hcHAudjEuUmVzdW1lUmVzcG9uc2USUQoKRXZpY3RMb2NhbBIgLmVuZ3JhbS5hcHAudjEuRXZpY3RMb2NhbFJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkV2aWN0TG9jYWxSZXNwb25zZRJOCglFdmljdElkbGUSHy5lbmdyYW0uYXBwLnYxLkV2aWN0SWRsZVJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLkV2aWN0SWRsZVJlc3BvbnNlElQKC0dldENvd1N0YXRlEiEuZW5ncmFtLmFwcC52MS5HZXRDb3dTdGF0ZVJlcXVlc3QaIi5lbmdyYW0uYXBwLnYxLkdldENvd1N0YXRlUmVzcG9uc2USYAoPTGlzdENoZWNrcG9pbnRzEiUuZW5ncmFtLmFwcC52MS5MaXN0Q2hlY2twb2ludHNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q2hlY2twb2ludHNSZXNwb25zZRJWCgtHZXRBcnRpZmFjdBIhLmVuZ3JhbS5hcHAudjEuR2V0QXJ0aWZhY3RSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRBcnRpZmFjdFJlc3BvbnNlMAESdQoWQ3JlYXRlQXJ0aWZhY3RGcm9tUGF0aBIsLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQXJ0aWZhY3RGcm9tUGF0aFJlcXVlc3QaLS5lbmdyYW0uYXBwLnYxLkNyZWF0ZUFydGlmYWN0RnJvbVBhdGhSZXNwb25zZTJlChFTaGVsbFJlbGF5U2VydmljZRJQCgVSZWxheRIgLmVuZ3JhbS5hcHAudjEuUmVsYXlTaGVsbFJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLlJlbGF5U2hlbGxSZXNwb25zZSgBMAEyYgoQUG9ydFJlbGF5U2VydmljZRJOCgVSZWxheRIfLmVuZ3JhbS5hcHAudjEuUmVsYXlQb3J0UmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuUmVsYXlQb3J0UmVzcG9uc2UoATABYgZwcm90bzM", [file_engram_app_v1_oauth]); /** * @generated from message engram.app.v1.ListSessionsRequest @@ -684,6 +684,196 @@ export type DequeueQueuedPromptResponse = Message<"engram.app.v1.DequeueQueuedPr export const DequeueQueuedPromptResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_session, 19); +/** + * @generated from message engram.app.v1.McpWritePolicyRef + */ +export type McpWritePolicyRef = Message<"engram.app.v1.McpWritePolicyRef"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: int64 policy_revision = 2; + */ + policyRevision: bigint; + + /** + * @generated from field: int64 binding_epoch = 3; + */ + bindingEpoch: bigint; +}; + +/** + * Describes the message engram.app.v1.McpWritePolicyRef. + * Use `create(McpWritePolicyRefSchema)` to create a new message. + */ +export const McpWritePolicyRefSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 20); + +/** + * @generated from message engram.app.v1.McpTrustedWritePolicy + */ +export type McpTrustedWritePolicy = Message<"engram.app.v1.McpTrustedWritePolicy"> & { + /** + * @generated from field: engram.app.v1.McpWritePolicyRef ref = 1; + */ + ref?: McpWritePolicyRef; + + /** + * @generated from field: string write_eligibility = 2; + */ + writeEligibility: string; + + /** + * @generated from field: string cause = 3; + */ + cause: string; + + /** + * @generated from field: string accepted_input_id = 4; + */ + acceptedInputId: string; + + /** + * @generated from field: optional string authenticated_actor_id = 5; + */ + authenticatedActorId?: string; + + /** + * @generated from field: optional string prompt_id = 6; + */ + promptId?: string; + + /** + * @generated from field: optional string plan_tool_call_id = 7; + */ + planToolCallId?: string; + + /** + * @generated from field: optional string plan_request_digest = 8; + */ + planRequestDigest?: string; + + /** + * @generated from field: repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 9; + */ + ancestorPolicyRefs: McpWritePolicyRef[]; + + /** + * @generated from field: bool current = 10; + */ + current: boolean; + + /** + * @generated from field: bool initialized = 11; + */ + initialized: boolean; +}; + +/** + * Describes the message engram.app.v1.McpTrustedWritePolicy. + * Use `create(McpTrustedWritePolicySchema)` to create a new message. + */ +export const McpTrustedWritePolicySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 21); + +/** + * @generated from message engram.app.v1.McpPlanReview + */ +export type McpPlanReview = Message<"engram.app.v1.McpPlanReview"> & { + /** + * @generated from field: engram.app.v1.McpWritePolicyRef policy_ref = 1; + */ + policyRef?: McpWritePolicyRef; + + /** + * @generated from field: string tool_call_id = 2; + */ + toolCallId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: int64 event_idx = 4; + */ + eventIdx: bigint; + + /** + * @generated from field: string request_digest = 5; + */ + requestDigest: string; + + /** + * @generated from field: string args_json = 6; + */ + argsJson: string; +}; + +/** + * Describes the message engram.app.v1.McpPlanReview. + * Use `create(McpPlanReviewSchema)` to create a new message. + */ +export const McpPlanReviewSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 22); + +/** + * @generated from message engram.app.v1.GetMcpWritePolicyRequest + */ +export type GetMcpWritePolicyRequest = Message<"engram.app.v1.GetMcpWritePolicyRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: optional string plan_tool_call_id = 2; + */ + planToolCallId?: string; +}; + +/** + * Describes the message engram.app.v1.GetMcpWritePolicyRequest. + * Use `create(GetMcpWritePolicyRequestSchema)` to create a new message. + */ +export const GetMcpWritePolicyRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 23); + +/** + * @generated from message engram.app.v1.GetMcpWritePolicyResponse + */ +export type GetMcpWritePolicyResponse = Message<"engram.app.v1.GetMcpWritePolicyResponse"> & { + /** + * @generated from field: engram.app.v1.McpTrustedWritePolicy policy = 1; + */ + policy?: McpTrustedWritePolicy; + + /** + * @generated from field: engram.app.v1.McpPlanReview plan_review = 2; + */ + planReview?: McpPlanReview; + + /** + * @generated from field: string reason = 3; + */ + reason: string; + + /** + * @generated from field: repeated string allowed_actions = 4; + */ + allowedActions: string[]; +}; + +/** + * Describes the message engram.app.v1.GetMcpWritePolicyResponse. + * Use `create(GetMcpWritePolicyResponseSchema)` to create a new message. + */ +export const GetMcpWritePolicyResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_session, 24); + /** * @generated from message engram.app.v1.CompleteToolCallRequest */ @@ -708,6 +898,13 @@ export type CompleteToolCallRequest = Message<"engram.app.v1.CompleteToolCallReq * @generated from field: string result_json = 3; */ resultJson: string; + + /** + * Optional exact server review. Only the authenticated human handler consumes it. + * + * @generated from field: engram.app.v1.McpPlanReview expected_mcp_plan_review = 4; + */ + expectedMcpPlanReview?: McpPlanReview; }; /** @@ -715,7 +912,7 @@ export type CompleteToolCallRequest = Message<"engram.app.v1.CompleteToolCallReq * Use `create(CompleteToolCallRequestSchema)` to create a new message. */ export const CompleteToolCallRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 20); + messageDesc(file_engram_app_v1_session, 25); /** * @generated from message engram.app.v1.CompleteToolCallResponse @@ -737,7 +934,7 @@ export type CompleteToolCallResponse = Message<"engram.app.v1.CompleteToolCallRe * Use `create(CompleteToolCallResponseSchema)` to create a new message. */ export const CompleteToolCallResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 21); + messageDesc(file_engram_app_v1_session, 26); /** * @generated from message engram.app.v1.StreamEventsRequest @@ -774,7 +971,7 @@ export type StreamEventsRequest = Message<"engram.app.v1.StreamEventsRequest"> & * Use `create(StreamEventsRequestSchema)` to create a new message. */ export const StreamEventsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 22); + messageDesc(file_engram_app_v1_session, 27); /** * The typed event ENVELOPE. payload_json is NORMATIVELY the JSON object @@ -812,7 +1009,7 @@ export type SessionEvent = Message<"engram.app.v1.SessionEvent"> & { * Use `create(SessionEventSchema)` to create a new message. */ export const SessionEventSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 23); + messageDesc(file_engram_app_v1_session, 28); /** * @generated from message engram.app.v1.ListSessionEventsRequest @@ -878,7 +1075,7 @@ export type ListSessionEventsRequest = Message<"engram.app.v1.ListSessionEventsR * Use `create(ListSessionEventsRequestSchema)` to create a new message. */ export const ListSessionEventsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 24); + messageDesc(file_engram_app_v1_session, 29); /** * @generated from message engram.app.v1.ListSessionEventsResponse @@ -907,7 +1104,7 @@ export type ListSessionEventsResponse = Message<"engram.app.v1.ListSessionEvents * Use `create(ListSessionEventsResponseSchema)` to create a new message. */ export const ListSessionEventsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 25); + messageDesc(file_engram_app_v1_session, 30); /** * @generated from message engram.app.v1.ExecRequest @@ -977,7 +1174,7 @@ export type ExecRequest = Message<"engram.app.v1.ExecRequest"> & { * Use `create(ExecRequestSchema)` to create a new message. */ export const ExecRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 26); + messageDesc(file_engram_app_v1_session, 31); /** * @generated from message engram.app.v1.CancelExecRequest @@ -999,7 +1196,7 @@ export type CancelExecRequest = Message<"engram.app.v1.CancelExecRequest"> & { * Use `create(CancelExecRequestSchema)` to create a new message. */ export const CancelExecRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 27); + messageDesc(file_engram_app_v1_session, 32); /** * @generated from message engram.app.v1.CancelExecResponse @@ -1012,7 +1209,7 @@ export type CancelExecResponse = Message<"engram.app.v1.CancelExecResponse"> & { * Use `create(CancelExecResponseSchema)` to create a new message. */ export const CancelExecResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 28); + messageDesc(file_engram_app_v1_session, 33); /** * @generated from message engram.app.v1.WriteFileMetadata @@ -1055,7 +1252,7 @@ export type WriteFileMetadata = Message<"engram.app.v1.WriteFileMetadata"> & { * Use `create(WriteFileMetadataSchema)` to create a new message. */ export const WriteFileMetadataSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 29); + messageDesc(file_engram_app_v1_session, 34); /** * @generated from message engram.app.v1.WriteFileRequest @@ -1084,7 +1281,7 @@ export type WriteFileRequest = Message<"engram.app.v1.WriteFileRequest"> & { * Use `create(WriteFileRequestSchema)` to create a new message. */ export const WriteFileRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 30); + messageDesc(file_engram_app_v1_session, 35); /** * @generated from message engram.app.v1.WriteFileResponse @@ -1111,7 +1308,7 @@ export type WriteFileResponse = Message<"engram.app.v1.WriteFileResponse"> & { * Use `create(WriteFileResponseSchema)` to create a new message. */ export const WriteFileResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 31); + messageDesc(file_engram_app_v1_session, 36); /** * @generated from message engram.app.v1.ReadFileRequest @@ -1133,7 +1330,7 @@ export type ReadFileRequest = Message<"engram.app.v1.ReadFileRequest"> & { * Use `create(ReadFileRequestSchema)` to create a new message. */ export const ReadFileRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 32); + messageDesc(file_engram_app_v1_session, 37); /** * @generated from message engram.app.v1.ReadFileMetadata @@ -1165,7 +1362,7 @@ export type ReadFileMetadata = Message<"engram.app.v1.ReadFileMetadata"> & { * Use `create(ReadFileMetadataSchema)` to create a new message. */ export const ReadFileMetadataSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 33); + messageDesc(file_engram_app_v1_session, 38); /** * @generated from message engram.app.v1.ReadFileResponse @@ -1194,7 +1391,7 @@ export type ReadFileResponse = Message<"engram.app.v1.ReadFileResponse"> & { * Use `create(ReadFileResponseSchema)` to create a new message. */ export const ReadFileResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 34); + messageDesc(file_engram_app_v1_session, 39); /** * @generated from message engram.app.v1.CopyFilesRequest @@ -1221,7 +1418,7 @@ export type CopyFilesRequest = Message<"engram.app.v1.CopyFilesRequest"> & { * Use `create(CopyFilesRequestSchema)` to create a new message. */ export const CopyFilesRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 35); + messageDesc(file_engram_app_v1_session, 40); /** * @generated from message engram.app.v1.CopyFileResult @@ -1248,7 +1445,7 @@ export type CopyFileResult = Message<"engram.app.v1.CopyFileResult"> & { * Use `create(CopyFileResultSchema)` to create a new message. */ export const CopyFileResultSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 36); + messageDesc(file_engram_app_v1_session, 41); /** * @generated from message engram.app.v1.CopyFilesResponse @@ -1265,7 +1462,7 @@ export type CopyFilesResponse = Message<"engram.app.v1.CopyFilesResponse"> & { * Use `create(CopyFilesResponseSchema)` to create a new message. */ export const CopyFilesResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 37); + messageDesc(file_engram_app_v1_session, 42); /** * engram_core::types::ExecEvent is authoritative for the stream body: @@ -1317,7 +1514,7 @@ export type ExecOutput = Message<"engram.app.v1.ExecOutput"> & { * Use `create(ExecOutputSchema)` to create a new message. */ export const ExecOutputSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 38); + messageDesc(file_engram_app_v1_session, 43); /** * @generated from message engram.app.v1.ExecStarted @@ -1334,7 +1531,7 @@ export type ExecStarted = Message<"engram.app.v1.ExecStarted"> & { * Use `create(ExecStartedSchema)` to create a new message. */ export const ExecStartedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 39); + messageDesc(file_engram_app_v1_session, 44); /** * @generated from message engram.app.v1.ExecExit @@ -1362,7 +1559,7 @@ export type ExecExit = Message<"engram.app.v1.ExecExit"> & { * Use `create(ExecExitSchema)` to create a new message. */ export const ExecExitSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 40); + messageDesc(file_engram_app_v1_session, 45); /** * engram_core::types::ExecRusage. wall_ms is coordinator-measured wall @@ -1398,7 +1595,7 @@ export type ExecRusage = Message<"engram.app.v1.ExecRusage"> & { * Use `create(ExecRusageSchema)` to create a new message. */ export const ExecRusageSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 41); + messageDesc(file_engram_app_v1_session, 46); /** * @generated from message engram.app.v1.GetLogRequest @@ -1441,7 +1638,7 @@ export type GetLogRequest = Message<"engram.app.v1.GetLogRequest"> & { * Use `create(GetLogRequestSchema)` to create a new message. */ export const GetLogRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 42); + messageDesc(file_engram_app_v1_session, 47); /** * @generated from message engram.app.v1.GetLogResponse @@ -1468,7 +1665,7 @@ export type GetLogResponse = Message<"engram.app.v1.GetLogResponse"> & { * Use `create(GetLogResponseSchema)` to create a new message. */ export const GetLogResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 43); + messageDesc(file_engram_app_v1_session, 48); /** * @generated from message engram.app.v1.ConversationEntry @@ -1504,7 +1701,7 @@ export type ConversationEntry = Message<"engram.app.v1.ConversationEntry"> & { * Use `create(ConversationEntrySchema)` to create a new message. */ export const ConversationEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 44); + messageDesc(file_engram_app_v1_session, 49); /** * @generated from message engram.app.v1.SnapshotRequest @@ -1521,7 +1718,7 @@ export type SnapshotRequest = Message<"engram.app.v1.SnapshotRequest"> & { * Use `create(SnapshotRequestSchema)` to create a new message. */ export const SnapshotRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 45); + messageDesc(file_engram_app_v1_session, 50); /** * @generated from message engram.app.v1.SnapshotResponse @@ -1555,7 +1752,7 @@ export type SnapshotResponse = Message<"engram.app.v1.SnapshotResponse"> & { * Use `create(SnapshotResponseSchema)` to create a new message. */ export const SnapshotResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 46); + messageDesc(file_engram_app_v1_session, 51); /** * @generated from message engram.app.v1.EnsureBrowserRequest @@ -1572,7 +1769,7 @@ export type EnsureBrowserRequest = Message<"engram.app.v1.EnsureBrowserRequest"> * Use `create(EnsureBrowserRequestSchema)` to create a new message. */ export const EnsureBrowserRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 47); + messageDesc(file_engram_app_v1_session, 52); /** * @generated from message engram.app.v1.EnsureBrowserResponse @@ -1592,7 +1789,7 @@ export type EnsureBrowserResponse = Message<"engram.app.v1.EnsureBrowserResponse * Use `create(EnsureBrowserResponseSchema)` to create a new message. */ export const EnsureBrowserResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 48); + messageDesc(file_engram_app_v1_session, 53); /** * @generated from message engram.app.v1.EnsureIdeRequest @@ -1609,7 +1806,7 @@ export type EnsureIdeRequest = Message<"engram.app.v1.EnsureIdeRequest"> & { * Use `create(EnsureIdeRequestSchema)` to create a new message. */ export const EnsureIdeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 49); + messageDesc(file_engram_app_v1_session, 54); /** * @generated from message engram.app.v1.EnsureIdeResponse @@ -1629,7 +1826,7 @@ export type EnsureIdeResponse = Message<"engram.app.v1.EnsureIdeResponse"> & { * Use `create(EnsureIdeResponseSchema)` to create a new message. */ export const EnsureIdeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 50); + messageDesc(file_engram_app_v1_session, 55); /** * @generated from message engram.app.v1.ResumeRequest @@ -1646,7 +1843,7 @@ export type ResumeRequest = Message<"engram.app.v1.ResumeRequest"> & { * Use `create(ResumeRequestSchema)` to create a new message. */ export const ResumeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 51); + messageDesc(file_engram_app_v1_session, 56); /** * Same shape as SnapshotResponse (the HTTP resume handler reuses it); @@ -1681,7 +1878,7 @@ export type ResumeResponse = Message<"engram.app.v1.ResumeResponse"> & { * Use `create(ResumeResponseSchema)` to create a new message. */ export const ResumeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 52); + messageDesc(file_engram_app_v1_session, 57); /** * @generated from message engram.app.v1.EvictLocalRequest @@ -1698,7 +1895,7 @@ export type EvictLocalRequest = Message<"engram.app.v1.EvictLocalRequest"> & { * Use `create(EvictLocalRequestSchema)` to create a new message. */ export const EvictLocalRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 53); + messageDesc(file_engram_app_v1_session, 58); /** * Today's HTTP handler returns 202 Accepted with no body. @@ -1713,7 +1910,7 @@ export type EvictLocalResponse = Message<"engram.app.v1.EvictLocalResponse"> & { * Use `create(EvictLocalResponseSchema)` to create a new message. */ export const EvictLocalResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 54); + messageDesc(file_engram_app_v1_session, 59); /** * @generated from message engram.app.v1.EvictIdleRequest @@ -1730,7 +1927,7 @@ export type EvictIdleRequest = Message<"engram.app.v1.EvictIdleRequest"> & { * Use `create(EvictIdleRequestSchema)` to create a new message. */ export const EvictIdleRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 55); + messageDesc(file_engram_app_v1_session, 60); /** * Mirrors api/admin.rs EvictIdleResponse. The synchronous pipeline has @@ -1757,7 +1954,7 @@ export type EvictIdleResponse = Message<"engram.app.v1.EvictIdleResponse"> & { * Use `create(EvictIdleResponseSchema)` to create a new message. */ export const EvictIdleResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 56); + messageDesc(file_engram_app_v1_session, 61); /** * @generated from message engram.app.v1.GetCowStateRequest @@ -1774,7 +1971,7 @@ export type GetCowStateRequest = Message<"engram.app.v1.GetCowStateRequest"> & { * Use `create(GetCowStateRequestSchema)` to create a new message. */ export const GetCowStateRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 57); + messageDesc(file_engram_app_v1_session, 62); /** * @generated from message engram.app.v1.GetCowStateResponse @@ -1799,7 +1996,7 @@ export type GetCowStateResponse = Message<"engram.app.v1.GetCowStateResponse"> & * Use `create(GetCowStateResponseSchema)` to create a new message. */ export const GetCowStateResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 58); + messageDesc(file_engram_app_v1_session, 63); /** * Mirrors engram_coordinator::cow_state::CowStateView. One row per @@ -1892,7 +2089,7 @@ export type CowStateView = Message<"engram.app.v1.CowStateView"> & { * Use `create(CowStateViewSchema)` to create a new message. */ export const CowStateViewSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 59); + messageDesc(file_engram_app_v1_session, 64); /** * @generated from message engram.app.v1.ListCheckpointsRequest @@ -1909,7 +2106,7 @@ export type ListCheckpointsRequest = Message<"engram.app.v1.ListCheckpointsReque * Use `create(ListCheckpointsRequestSchema)` to create a new message. */ export const ListCheckpointsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 60); + messageDesc(file_engram_app_v1_session, 65); /** * @generated from message engram.app.v1.ListCheckpointsResponse @@ -1934,7 +2131,7 @@ export type ListCheckpointsResponse = Message<"engram.app.v1.ListCheckpointsResp * Use `create(ListCheckpointsResponseSchema)` to create a new message. */ export const ListCheckpointsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 61); + messageDesc(file_engram_app_v1_session, 66); /** * ADR 0028 A.log: one checkpoint in a session's chain. @@ -1986,7 +2183,7 @@ export type CheckpointSummary = Message<"engram.app.v1.CheckpointSummary"> & { * Use `create(CheckpointSummarySchema)` to create a new message. */ export const CheckpointSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 62); + messageDesc(file_engram_app_v1_session, 67); /** * @generated from message engram.app.v1.GetArtifactRequest @@ -2008,7 +2205,7 @@ export type GetArtifactRequest = Message<"engram.app.v1.GetArtifactRequest"> & { * Use `create(GetArtifactRequestSchema)` to create a new message. */ export const GetArtifactRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 63); + messageDesc(file_engram_app_v1_session, 68); /** * @generated from message engram.app.v1.GetArtifactResponse @@ -2039,7 +2236,7 @@ export type GetArtifactResponse = Message<"engram.app.v1.GetArtifactResponse"> & * Use `create(GetArtifactResponseSchema)` to create a new message. */ export const GetArtifactResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 64); + messageDesc(file_engram_app_v1_session, 69); /** * @generated from message engram.app.v1.ArtifactMetadata @@ -2069,7 +2266,7 @@ export type ArtifactMetadata = Message<"engram.app.v1.ArtifactMetadata"> & { * Use `create(ArtifactMetadataSchema)` to create a new message. */ export const ArtifactMetadataSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 65); + messageDesc(file_engram_app_v1_session, 70); /** * ADR 0026 trusted operator file pull: capture ANY file in the session @@ -2108,7 +2305,7 @@ export type CreateArtifactFromPathRequest = Message<"engram.app.v1.CreateArtifac * Use `create(CreateArtifactFromPathRequestSchema)` to create a new message. */ export const CreateArtifactFromPathRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 66); + messageDesc(file_engram_app_v1_session, 71); /** * @generated from message engram.app.v1.CreateArtifactFromPathResponse @@ -2135,7 +2332,7 @@ export type CreateArtifactFromPathResponse = Message<"engram.app.v1.CreateArtifa * Use `create(CreateArtifactFromPathResponseSchema)` to create a new message. */ export const CreateArtifactFromPathResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 67); + messageDesc(file_engram_app_v1_session, 72); /** * Frame kinds mirror the WS frames / host ProxyShellMessage 1:1. Request @@ -2194,7 +2391,7 @@ export type RelayShellRequest = Message<"engram.app.v1.RelayShellRequest"> & { * Use `create(RelayShellRequestSchema)` to create a new message. */ export const RelayShellRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 68); + messageDesc(file_engram_app_v1_session, 73); /** * @generated from message engram.app.v1.RelayShellResponse @@ -2241,7 +2438,7 @@ export type RelayShellResponse = Message<"engram.app.v1.RelayShellResponse"> & { * Use `create(RelayShellResponseSchema)` to create a new message. */ export const RelayShellResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 69); + messageDesc(file_engram_app_v1_session, 74); /** * @generated from message engram.app.v1.ShellOpen @@ -2258,7 +2455,7 @@ export type ShellOpen = Message<"engram.app.v1.ShellOpen"> & { * Use `create(ShellOpenSchema)` to create a new message. */ export const ShellOpenSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 70); + messageDesc(file_engram_app_v1_session, 75); /** * @generated from message engram.app.v1.ShellClose @@ -2280,7 +2477,7 @@ export type ShellClose = Message<"engram.app.v1.ShellClose"> & { * Use `create(ShellCloseSchema)` to create a new message. */ export const ShellCloseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 71); + messageDesc(file_engram_app_v1_session, 76); /** * Raw opaque bytes; no frame-kind taxonomy (cf. RelayShell*). Request @@ -2321,7 +2518,7 @@ export type RelayPortRequest = Message<"engram.app.v1.RelayPortRequest"> & { * Use `create(RelayPortRequestSchema)` to create a new message. */ export const RelayPortRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 72); + messageDesc(file_engram_app_v1_session, 77); /** * @generated from message engram.app.v1.RelayPortResponse @@ -2350,7 +2547,7 @@ export type RelayPortResponse = Message<"engram.app.v1.RelayPortResponse"> & { * Use `create(RelayPortResponseSchema)` to create a new message. */ export const RelayPortResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 73); + messageDesc(file_engram_app_v1_session, 78); /** * @generated from message engram.app.v1.PortOpen @@ -2374,7 +2571,7 @@ export type PortOpen = Message<"engram.app.v1.PortOpen"> & { * Use `create(PortOpenSchema)` to create a new message. */ export const PortOpenSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 74); + messageDesc(file_engram_app_v1_session, 79); /** * Clean teardown sentinel (see ProxyPortClose in host_service.proto). @@ -2389,7 +2586,7 @@ export type PortClose = Message<"engram.app.v1.PortClose"> & { * Use `create(PortCloseSchema)` to create a new message. */ export const PortCloseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_session, 75); + messageDesc(file_engram_app_v1_session, 80); /** * The orchestrator-facing app contract (ADR 0051 §2.3, rev 2026-06-10). @@ -2482,6 +2679,14 @@ export const SessionService: GenService<{ input: typeof CompleteToolCallRequestSchema; output: typeof CompleteToolCallResponseSchema; }, + /** + * @generated from rpc engram.app.v1.SessionService.GetMcpWritePolicy + */ + getMcpWritePolicy: { + methodKind: "unary"; + input: typeof GetMcpWritePolicyRequestSchema; + output: typeof GetMcpWritePolicyResponseSchema; + }, /** * Streams the event envelope itself, not a wrapped Response message. * buf:lint:ignore RPC_RESPONSE_STANDARD_NAME diff --git a/web/src/gen/engram/mcp/v1/control-CredentialControlService_connectquery.ts b/web/src/gen/engram/mcp/v1/control-CredentialControlService_connectquery.ts new file mode 100644 index 000000000..77890a1ce --- /dev/null +++ b/web/src/gen/engram/mcp/v1/control-CredentialControlService_connectquery.ts @@ -0,0 +1,40 @@ +// @generated by protoc-gen-connect-query v2.1.0 with parameter "target=ts" +// @generated from file engram/mcp/v1/control.proto (package engram.mcp.v1, syntax proto3) +/* eslint-disable */ + +import { CredentialControlService } from "./control_pb"; + +/** + * @generated from rpc engram.mcp.v1.CredentialControlService.PutSecret + */ +export const putSecret = CredentialControlService.method.putSecret; + +/** + * @generated from rpc engram.mcp.v1.CredentialControlService.BeginAuthorization + */ +export const beginAuthorization = CredentialControlService.method.beginAuthorization; + +/** + * @generated from rpc engram.mcp.v1.CredentialControlService.CancelAuthorization + */ +export const cancelAuthorization = CredentialControlService.method.cancelAuthorization; + +/** + * @generated from rpc engram.mcp.v1.CredentialControlService.CompleteAuthorization + */ +export const completeAuthorization = CredentialControlService.method.completeAuthorization; + +/** + * @generated from rpc engram.mcp.v1.CredentialControlService.GetStatus + */ +export const getStatus = CredentialControlService.method.getStatus; + +/** + * @generated from rpc engram.mcp.v1.CredentialControlService.Revoke + */ +export const revoke = CredentialControlService.method.revoke; + +/** + * @generated from rpc engram.mcp.v1.CredentialControlService.Lease + */ +export const lease = CredentialControlService.method.lease; diff --git a/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts b/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts deleted file mode 100644 index 1e8b72210..000000000 --- a/web/src/gen/engram/mcp/v1/control-CredentialControl_connectquery.ts +++ /dev/null @@ -1,40 +0,0 @@ -// @generated by protoc-gen-connect-query v2.1.0 with parameter "target=ts" -// @generated from file engram/mcp/v1/control.proto (package engram.mcp.v1, syntax proto3) -/* eslint-disable */ - -import { CredentialControl } from "./control_pb"; - -/** - * @generated from rpc engram.mcp.v1.CredentialControl.PutSecret - */ -export const putSecret = CredentialControl.method.putSecret; - -/** - * @generated from rpc engram.mcp.v1.CredentialControl.BeginAuthorization - */ -export const beginAuthorization = CredentialControl.method.beginAuthorization; - -/** - * @generated from rpc engram.mcp.v1.CredentialControl.CancelAuthorization - */ -export const cancelAuthorization = CredentialControl.method.cancelAuthorization; - -/** - * @generated from rpc engram.mcp.v1.CredentialControl.CompleteAuthorization - */ -export const completeAuthorization = CredentialControl.method.completeAuthorization; - -/** - * @generated from rpc engram.mcp.v1.CredentialControl.GetStatus - */ -export const getStatus = CredentialControl.method.getStatus; - -/** - * @generated from rpc engram.mcp.v1.CredentialControl.Revoke - */ -export const revoke = CredentialControl.method.revoke; - -/** - * @generated from rpc engram.mcp.v1.CredentialControl.Lease - */ -export const lease = CredentialControl.method.lease; diff --git a/web/src/gen/engram/mcp/v1/control_pb.ts b/web/src/gen/engram/mcp/v1/control_pb.ts index c7c78e197..31061b23b 100644 --- a/web/src/gen/engram/mcp/v1/control_pb.ts +++ b/web/src/gen/engram/mcp/v1/control_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/control.proto. */ export const file_engram_mcp_v1_control: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDIoEBChpDYW5jZWxBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJEhAKCGFjdG9yX2lkGAUgASgJInAKG0NhbmNlbEF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJKowBCg5DcmVkZW50aWFsS2luZBIfChtDUkVERU5USUFMX0tJTkRfVU5TUEVDSUZJRUQQABIbChdDUkVERU5USUFMX0tJTkRfQVBJX0tFWRABEiEKHUNSRURFTlRJQUxfS0lORF9DTElFTlRfU0VDUkVUEAISGQoVQ1JFREVOVElBTF9LSU5EX09BVVRIEAMy/gQKEUNyZWRlbnRpYWxDb250cm9sEk0KCVB1dFNlY3JldBIfLmVuZ3JhbS5tY3AudjEuUHV0U2VjcmV0UmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmwKE0NhbmNlbEF1dGhvcml6YXRpb24SKS5lbmdyYW0ubWNwLnYxLkNhbmNlbEF1dGhvcml6YXRpb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DYW5jZWxBdXRob3JpemF0aW9uUmVzcG9uc2USZQoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0Gh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEk0KCUdldFN0YXR1cxIfLmVuZ3JhbS5tY3AudjEuR2V0U3RhdHVzUmVxdWVzdBofLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cxJHCgZSZXZva2USHC5lbmdyYW0ubWNwLnYxLlJldm9rZVJlcXVlc3QaHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMSQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); + fileDesc("ChtlbmdyYW0vbWNwL3YxL2NvbnRyb2wucHJvdG8SDWVuZ3JhbS5tY3AudjEiMAoLRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSKYAQoNQ2hhbmdlQ29udGV4dBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgCIAEoAxIpCiFwcmV2aW91c19hdXRob3JpemF0aW9uX2dlbmVyYXRpb24YAyABKAMSLAoIcmVzb3VyY2UYBCABKAsyGi5lbmdyYW0ubWNwLnYxLkRlc3RpbmF0aW9uIt8BChBBdXRob3JpdHlDb250ZXh0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSFwoPY29uZmlnX3JldmlzaW9uGAIgASgDEiAKGGF1dGhvcml6YXRpb25fZ2VuZXJhdGlvbhgDIAEoAxIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEiwKCHJlc291cmNlGAUgASgLMhouZW5ncmFtLm1jcC52MS5EZXN0aW5hdGlvbhIVCg1wb2xpY3lfZGlnZXN0GAYgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgHIAEoCSKUAQoTQ3JlZGVudGlhbFJlZmVyZW5jZRIrCgRraW5kGAEgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBIMCgRuYW1lGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoAxIXCg9yZWdpc3RyYXRpb25faWQYBSABKAkirgEKEENyZWRlbnRpYWxTdGF0dXMSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USDwoHdmVyc2lvbhgEIAEoAxIOCgZzdGF0dXMYBSABKAki5wEKEFB1dFNlY3JldFJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIrCgRraW5kGAMgASgOMh0uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsS2luZBI0CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkSEAoIYWN0b3JfaWQYByABKAkijwIKGUJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SLQoHY29udGV4dBgCIAEoCzIcLmVuZ3JhbS5tY3AudjEuQ2hhbmdlQ29udGV4dBIXCg9yZWdpc3RyYXRpb25faWQYAyABKAkSOQoNY2xpZW50X3NlY3JldBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZRIdChBleHBlY3RlZF92ZXJzaW9uGAUgASgDSACIAQESEAoIYWN0b3JfaWQYBiABKAkSFwoPc2hhcmluZ19jb25zZW50GAcgASgIQhMKEV9leHBlY3RlZF92ZXJzaW9uIqABChpCZWdpbkF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEjAKB2NvbnRleHQYAyABKAsyHy5lbmdyYW0ubWNwLnYxLkF1dGhvcml0eUNvbnRleHQSGQoRYXV0aG9yaXphdGlvbl91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAyLxAQocQ29tcGxldGVBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIwCgdjb250ZXh0GAIgASgLMh8uZW5ncmFtLm1jcC52MS5BdXRob3JpdHlDb250ZXh0EhcKD3JlZ2lzdHJhdGlvbl9pZBgDIAEoCRIPCgdmbG93X2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEhQKDHJlZGlyZWN0X3VyaRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIMCgRjb2RlGAggASgJEhAKCGFjdG9yX2lkGAkgASgJQgkKB19pc3N1ZXIicgoQR2V0U3RhdHVzUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEjUKCXJlZmVyZW5jZRgDIAEoCzIiLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFJlZmVyZW5jZSJmCg1SZXZva2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjEKCGV4cGVjdGVkGAIgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzEhAKCGFjdG9yX2lkGAMgASgJInQKDldvcmtlcklkZW50aXR5EhAKCHByb3RvY29sGAEgASgNEg8KB2Jvb3RfaWQYAiABKAkSDwoHem9uZV9pZBgDIAEoCRIVCg1wb2xpY3lfZGlnZXN0GAQgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgFIAEoCSJCCglBZG1pc3Npb24SCgoCaWQYASABKAkSFQoNZGVwbG95bWVudF9pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgDIjsKE0FkbWluaXN0cmF0aW9uU2NvcGUSEgoKcmVxdWVzdF9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCSJgCg5FeGVjdXRpb25TY29wZRISCgpzZXNzaW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIUCgx0b29sX2NhbGxfaWQYAyABKAkSFAoMYmluZGluZ19oYXNoGAQgASgJItADCgxMZWFzZVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SMAoHY29udGV4dBgCIAEoCzIfLmVuZ3JhbS5tY3AudjEuQXV0aG9yaXR5Q29udGV4dBI1CglyZWZlcmVuY2UYAyABKAsyIi5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxSZWZlcmVuY2USLQoGd29ya2VyGAQgASgLMh0uZW5ncmFtLm1jcC52MS5Xb3JrZXJJZGVudGl0eRIRCglpbnRlbnRfaWQYBSABKAkSFgoOcmVxdWVzdF9kaWdlc3QYBiABKAkSEQoJb3BlcmF0aW9uGAcgASgJEhIKCmV4cGlyZXNfYXQYCCABKAMSHAoUb3BlcmF0aW9uX2V4cGlyZXNfYXQYDCABKAMSKwoJYWRtaXNzaW9uGAkgASgLMhguZW5ncmFtLm1jcC52MS5BZG1pc3Npb24SPAoOYWRtaW5pc3RyYXRpb24YCiABKAsyIi5lbmdyYW0ubWNwLnYxLkFkbWluaXN0cmF0aW9uU2NvcGVIABIyCglleGVjdXRpb24YCyABKAsyHS5lbmdyYW0ubWNwLnYxLkV4ZWN1dGlvblNjb3BlSABCBwoFc2NvcGUihQEKDUxlYXNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGaGVhZGVyGAIgASgJEg0KBXZhbHVlGAMgASgJEhIKCmV4cGlyZXNfYXQYBCABKAMSEQoJaW50ZW50X2lkGAUgASgJEhwKFG9wZXJhdGlvbl9leHBpcmVzX2F0GAYgASgDIoEBChpDYW5jZWxBdXRob3JpemF0aW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJEhAKCGFjdG9yX2lkGAUgASgJInAKG0NhbmNlbEF1dGhvcml6YXRpb25SZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIPCgdmbG93X2lkGAIgASgJEhUKDWNvbm5lY3Rpb25faWQYAyABKAkSFwoPcmVnaXN0cmF0aW9uX2lkGAQgASgJIkQKEVB1dFNlY3JldFJlc3BvbnNlEi8KBnN0YXR1cxgBIAEoCzIfLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cyJQCh1Db21wbGV0ZUF1dGhvcml6YXRpb25SZXNwb25zZRIvCgZzdGF0dXMYASABKAsyHy5lbmdyYW0ubWNwLnYxLkNyZWRlbnRpYWxTdGF0dXMiRAoRR2V0U3RhdHVzUmVzcG9uc2USLwoGc3RhdHVzGAEgASgLMh8uZW5ncmFtLm1jcC52MS5DcmVkZW50aWFsU3RhdHVzIkEKDlJldm9rZVJlc3BvbnNlEi8KBnN0YXR1cxgBIAEoCzIfLmVuZ3JhbS5tY3AudjEuQ3JlZGVudGlhbFN0YXR1cyqMAQoOQ3JlZGVudGlhbEtpbmQSHwobQ1JFREVOVElBTF9LSU5EX1VOU1BFQ0lGSUVEEAASGwoXQ1JFREVOVElBTF9LSU5EX0FQSV9LRVkQARIhCh1DUkVERU5USUFMX0tJTkRfQ0xJRU5UX1NFQ1JFVBACEhkKFUNSRURFTlRJQUxfS0lORF9PQVVUSBADMpIFChhDcmVkZW50aWFsQ29udHJvbFNlcnZpY2USTgoJUHV0U2VjcmV0Eh8uZW5ncmFtLm1jcC52MS5QdXRTZWNyZXRSZXF1ZXN0GiAuZW5ncmFtLm1jcC52MS5QdXRTZWNyZXRSZXNwb25zZRJpChJCZWdpbkF1dGhvcml6YXRpb24SKC5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkJlZ2luQXV0aG9yaXphdGlvblJlc3BvbnNlEmwKE0NhbmNlbEF1dGhvcml6YXRpb24SKS5lbmdyYW0ubWNwLnYxLkNhbmNlbEF1dGhvcml6YXRpb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DYW5jZWxBdXRob3JpemF0aW9uUmVzcG9uc2UScgoVQ29tcGxldGVBdXRob3JpemF0aW9uEisuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5Db21wbGV0ZUF1dGhvcml6YXRpb25SZXNwb25zZRJOCglHZXRTdGF0dXMSHy5lbmdyYW0ubWNwLnYxLkdldFN0YXR1c1JlcXVlc3QaIC5lbmdyYW0ubWNwLnYxLkdldFN0YXR1c1Jlc3BvbnNlEkUKBlJldm9rZRIcLmVuZ3JhbS5tY3AudjEuUmV2b2tlUmVxdWVzdBodLmVuZ3JhbS5tY3AudjEuUmV2b2tlUmVzcG9uc2USQgoFTGVhc2USGy5lbmdyYW0ubWNwLnYxLkxlYXNlUmVxdWVzdBocLmVuZ3JhbS5tY3AudjEuTGVhc2VSZXNwb25zZWIGcHJvdG8z"); /** * @generated from message engram.mcp.v1.Destination @@ -746,6 +746,74 @@ export type CancelAuthorizationResponse = Message<"engram.mcp.v1.CancelAuthoriza export const CancelAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_control, 18); +/** + * @generated from message engram.mcp.v1.PutSecretResponse + */ +export type PutSecretResponse = Message<"engram.mcp.v1.PutSecretResponse"> & { + /** + * @generated from field: engram.mcp.v1.CredentialStatus status = 1; + */ + status?: CredentialStatus; +}; + +/** + * Describes the message engram.mcp.v1.PutSecretResponse. + * Use `create(PutSecretResponseSchema)` to create a new message. + */ +export const PutSecretResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 19); + +/** + * @generated from message engram.mcp.v1.CompleteAuthorizationResponse + */ +export type CompleteAuthorizationResponse = Message<"engram.mcp.v1.CompleteAuthorizationResponse"> & { + /** + * @generated from field: engram.mcp.v1.CredentialStatus status = 1; + */ + status?: CredentialStatus; +}; + +/** + * Describes the message engram.mcp.v1.CompleteAuthorizationResponse. + * Use `create(CompleteAuthorizationResponseSchema)` to create a new message. + */ +export const CompleteAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 20); + +/** + * @generated from message engram.mcp.v1.GetStatusResponse + */ +export type GetStatusResponse = Message<"engram.mcp.v1.GetStatusResponse"> & { + /** + * @generated from field: engram.mcp.v1.CredentialStatus status = 1; + */ + status?: CredentialStatus; +}; + +/** + * Describes the message engram.mcp.v1.GetStatusResponse. + * Use `create(GetStatusResponseSchema)` to create a new message. + */ +export const GetStatusResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 21); + +/** + * @generated from message engram.mcp.v1.RevokeResponse + */ +export type RevokeResponse = Message<"engram.mcp.v1.RevokeResponse"> & { + /** + * @generated from field: engram.mcp.v1.CredentialStatus status = 1; + */ + status?: CredentialStatus; +}; + +/** + * Describes the message engram.mcp.v1.RevokeResponse. + * Use `create(RevokeResponseSchema)` to create a new message. + */ +export const RevokeResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_control, 22); + /** * @generated from enum engram.mcp.v1.CredentialKind */ @@ -780,19 +848,19 @@ export const CredentialKindSchema: GenEnum = /*@__PURE__*/ /** * Internal protocol 1. Served only on the separate mTLS listener. * - * @generated from service engram.mcp.v1.CredentialControl + * @generated from service engram.mcp.v1.CredentialControlService */ -export const CredentialControl: GenService<{ +export const CredentialControlService: GenService<{ /** - * @generated from rpc engram.mcp.v1.CredentialControl.PutSecret + * @generated from rpc engram.mcp.v1.CredentialControlService.PutSecret */ putSecret: { methodKind: "unary"; input: typeof PutSecretRequestSchema; - output: typeof CredentialStatusSchema; + output: typeof PutSecretResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.BeginAuthorization + * @generated from rpc engram.mcp.v1.CredentialControlService.BeginAuthorization */ beginAuthorization: { methodKind: "unary"; @@ -800,7 +868,7 @@ export const CredentialControl: GenService<{ output: typeof BeginAuthorizationResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.CancelAuthorization + * @generated from rpc engram.mcp.v1.CredentialControlService.CancelAuthorization */ cancelAuthorization: { methodKind: "unary"; @@ -808,31 +876,31 @@ export const CredentialControl: GenService<{ output: typeof CancelAuthorizationResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.CompleteAuthorization + * @generated from rpc engram.mcp.v1.CredentialControlService.CompleteAuthorization */ completeAuthorization: { methodKind: "unary"; input: typeof CompleteAuthorizationRequestSchema; - output: typeof CredentialStatusSchema; + output: typeof CompleteAuthorizationResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.GetStatus + * @generated from rpc engram.mcp.v1.CredentialControlService.GetStatus */ getStatus: { methodKind: "unary"; input: typeof GetStatusRequestSchema; - output: typeof CredentialStatusSchema; + output: typeof GetStatusResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.Revoke + * @generated from rpc engram.mcp.v1.CredentialControlService.Revoke */ revoke: { methodKind: "unary"; input: typeof RevokeRequestSchema; - output: typeof CredentialStatusSchema; + output: typeof RevokeResponseSchema; }, /** - * @generated from rpc engram.mcp.v1.CredentialControl.Lease + * @generated from rpc engram.mcp.v1.CredentialControlService.Lease */ lease: { methodKind: "unary"; diff --git a/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts b/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts new file mode 100644 index 000000000..32996e105 --- /dev/null +++ b/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts @@ -0,0 +1,25 @@ +// @generated by protoc-gen-connect-query v2.1.0 with parameter "target=ts" +// @generated from file engram/mcp/v1/session_policy.proto (package engram.mcp.v1, syntax proto3) +/* eslint-disable */ + +import { SessionPolicyService } from "./session_policy_pb"; + +/** + * @generated from rpc engram.mcp.v1.SessionPolicyService.CreatePolicySession + */ +export const createPolicySession = SessionPolicyService.method.createPolicySession; + +/** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ReadWritePolicy + */ +export const readWritePolicy = SessionPolicyService.method.readWritePolicy; + +/** + * @generated from rpc engram.mcp.v1.SessionPolicyService.AcceptPlanExit + */ +export const acceptPlanExit = SessionPolicyService.method.acceptPlanExit; + +/** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ValidateDispatchFence + */ +export const validateDispatchFence = SessionPolicyService.method.validateDispatchFence; diff --git a/web/src/gen/engram/mcp/v1/session_policy_pb.ts b/web/src/gen/engram/mcp/v1/session_policy_pb.ts new file mode 100644 index 000000000..a2c12482d --- /dev/null +++ b/web/src/gen/engram/mcp/v1/session_policy_pb.ts @@ -0,0 +1,273 @@ +// @generated by protoc-gen-es v2.2.5 with parameter "target=ts" +// @generated from file engram/mcp/v1/session_policy.proto (package engram.mcp.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { CreateSessionRequest, CreateSessionResponse, GetMcpWritePolicyResponse, McpPlanReview, McpTrustedWritePolicy, McpWritePolicyRef } from "../../app/v1/session_pb"; +import { file_engram_app_v1_session } from "../../app/v1/session_pb"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file engram/mcp/v1/session_policy.proto. + */ +export const file_engram_mcp_v1_session_policy: GenFile = /*@__PURE__*/ + fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIo0CChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYiQQodVmFsaWRhdGVEaXNwYXRjaEZlbmNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGc3RhdHVzGAIgASgJIlQKG0NyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRI1CgdzZXNzaW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVzcG9uc2UiUgoXUmVhZFdyaXRlUG9saWN5UmVzcG9uc2USNwoFc3RhdGUYASABKAsyKC5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVzcG9uc2UiTgoWQWNjZXB0UGxhbkV4aXRSZXNwb25zZRI0CgZwb2xpY3kYASABKAsyJC5lbmdyYW0uYXBwLnYxLk1jcFRydXN0ZWRXcml0ZVBvbGljeTK5AwoUU2Vzc2lvblBvbGljeVNlcnZpY2USbAoTQ3JlYXRlUG9saWN5U2Vzc2lvbhIpLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlcXVlc3QaKi5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRJgCg9SZWFkV3JpdGVQb2xpY3kSJS5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlcXVlc3QaJi5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlc3BvbnNlEl0KDkFjY2VwdFBsYW5FeGl0EiQuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlcXVlc3QaJS5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVzcG9uc2UScgoVVmFsaWRhdGVEaXNwYXRjaEZlbmNlEisuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZWIGcHJvdG8z", [file_engram_app_v1_session]); + +/** + * @generated from message engram.mcp.v1.CreatePolicySessionRequest + */ +export type CreatePolicySessionRequest = Message<"engram.mcp.v1.CreatePolicySessionRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.app.v1.CreateSessionRequest create = 2; + */ + create?: CreateSessionRequest; + + /** + * @generated from field: string actor_id = 3; + */ + actorId: string; + + /** + * @generated from field: string input_id = 4; + */ + inputId: string; + + /** + * @generated from field: optional string parent_session_id = 5; + */ + parentSessionId?: string; +}; + +/** + * Describes the message engram.mcp.v1.CreatePolicySessionRequest. + * Use `create(CreatePolicySessionRequestSchema)` to create a new message. + */ +export const CreatePolicySessionRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 0); + +/** + * @generated from message engram.mcp.v1.ReadWritePolicyRequest + */ +export type ReadWritePolicyRequest = Message<"engram.mcp.v1.ReadWritePolicyRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: optional string plan_tool_call_id = 3; + */ + planToolCallId?: string; +}; + +/** + * Describes the message engram.mcp.v1.ReadWritePolicyRequest. + * Use `create(ReadWritePolicyRequestSchema)` to create a new message. + */ +export const ReadWritePolicyRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 1); + +/** + * @generated from message engram.mcp.v1.AcceptPlanExitRequest + */ +export type AcceptPlanExitRequest = Message<"engram.mcp.v1.AcceptPlanExitRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.app.v1.McpPlanReview expected = 2; + */ + expected?: McpPlanReview; + + /** + * @generated from field: string actor_id = 3; + */ + actorId: string; + + /** + * @generated from field: string result_json = 4; + */ + resultJson: string; +}; + +/** + * Describes the message engram.mcp.v1.AcceptPlanExitRequest. + * Use `create(AcceptPlanExitRequestSchema)` to create a new message. + */ +export const AcceptPlanExitRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 2); + +/** + * @generated from message engram.mcp.v1.ValidateDispatchFenceRequest + */ +export type ValidateDispatchFenceRequest = Message<"engram.mcp.v1.ValidateDispatchFenceRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 4; + */ + toolCallId: string; + + /** + * @generated from field: engram.app.v1.McpWritePolicyRef accepted_policy_ref = 5; + */ + acceptedPolicyRef?: McpWritePolicyRef; + + /** + * @generated from field: string accepted_write_eligibility = 6; + */ + acceptedWriteEligibility: string; + + /** + * @generated from field: repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 7; + */ + ancestorPolicyRefs: McpWritePolicyRef[]; +}; + +/** + * Describes the message engram.mcp.v1.ValidateDispatchFenceRequest. + * Use `create(ValidateDispatchFenceRequestSchema)` to create a new message. + */ +export const ValidateDispatchFenceRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 3); + +/** + * @generated from message engram.mcp.v1.ValidateDispatchFenceResponse + */ +export type ValidateDispatchFenceResponse = Message<"engram.mcp.v1.ValidateDispatchFenceResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string status = 2; + */ + status: string; +}; + +/** + * Describes the message engram.mcp.v1.ValidateDispatchFenceResponse. + * Use `create(ValidateDispatchFenceResponseSchema)` to create a new message. + */ +export const ValidateDispatchFenceResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 4); + +/** + * @generated from message engram.mcp.v1.CreatePolicySessionResponse + */ +export type CreatePolicySessionResponse = Message<"engram.mcp.v1.CreatePolicySessionResponse"> & { + /** + * @generated from field: engram.app.v1.CreateSessionResponse session = 1; + */ + session?: CreateSessionResponse; +}; + +/** + * Describes the message engram.mcp.v1.CreatePolicySessionResponse. + * Use `create(CreatePolicySessionResponseSchema)` to create a new message. + */ +export const CreatePolicySessionResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 5); + +/** + * @generated from message engram.mcp.v1.ReadWritePolicyResponse + */ +export type ReadWritePolicyResponse = Message<"engram.mcp.v1.ReadWritePolicyResponse"> & { + /** + * @generated from field: engram.app.v1.GetMcpWritePolicyResponse state = 1; + */ + state?: GetMcpWritePolicyResponse; +}; + +/** + * Describes the message engram.mcp.v1.ReadWritePolicyResponse. + * Use `create(ReadWritePolicyResponseSchema)` to create a new message. + */ +export const ReadWritePolicyResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 6); + +/** + * @generated from message engram.mcp.v1.AcceptPlanExitResponse + */ +export type AcceptPlanExitResponse = Message<"engram.mcp.v1.AcceptPlanExitResponse"> & { + /** + * @generated from field: engram.app.v1.McpTrustedWritePolicy policy = 1; + */ + policy?: McpTrustedWritePolicy; +}; + +/** + * Describes the message engram.mcp.v1.AcceptPlanExitResponse. + * Use `create(AcceptPlanExitResponseSchema)` to create a new message. + */ +export const AcceptPlanExitResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 7); + +/** + * Dedicated pinned mTLS only. Create reuses the ordinary creation core. + * + * @generated from service engram.mcp.v1.SessionPolicyService + */ +export const SessionPolicyService: GenService<{ + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.CreatePolicySession + */ + createPolicySession: { + methodKind: "unary"; + input: typeof CreatePolicySessionRequestSchema; + output: typeof CreatePolicySessionResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ReadWritePolicy + */ + readWritePolicy: { + methodKind: "unary"; + input: typeof ReadWritePolicyRequestSchema; + output: typeof ReadWritePolicyResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.AcceptPlanExit + */ + acceptPlanExit: { + methodKind: "unary"; + input: typeof AcceptPlanExitRequestSchema; + output: typeof AcceptPlanExitResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ValidateDispatchFence + */ + validateDispatchFence: { + methodKind: "unary"; + input: typeof ValidateDispatchFenceRequestSchema; + output: typeof ValidateDispatchFenceResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_engram_mcp_v1_session_policy, 0); + From 87c9103cf4263c77261d51df2f070e29cf828510 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 02:14:29 -0500 Subject: [PATCH 018/279] Bind MCP tools to immutable session authority --- .../drizzle/0105_mcp_binding_envelope.sql | 1 + orchestrator/drizzle/meta/0105_snapshot.json | 8439 +++++++++++++++++ orchestrator/drizzle/meta/_journal.json | 7 + .../connection-credential-broker.test.ts | 43 + .../src/__tests__/mcp-binding-fixture.ts | 64 + .../src/__tests__/mcp-binding-store.test.ts | 174 + .../src/__tests__/mcp-child-bindings.test.ts | 86 + .../__tests__/mcp-session-bindings.test.ts | 188 + .../src/__tests__/mcp-session-create.test.ts | 199 + .../__tests__/mcp-session-resolver.test.ts | 161 + orchestrator/src/__tests__/tasks.test.ts | 1 + orchestrator/src/db/schema.ts | 6 +- orchestrator/src/integrations/grants.ts | 2 + orchestrator/src/mcp/binding-store.ts | 183 + orchestrator/src/mcp/session-bindings.ts | 323 + orchestrator/src/mcp/session-child.ts | 42 + orchestrator/src/mcp/session-create.ts | 170 + orchestrator/src/mcp/session-resolver.ts | 134 + .../routes/connection-credential-broker.ts | 3 + orchestrator/src/rpc/task-create.ts | 30 +- orchestrator/src/tools/__tests__/exec.test.ts | 19 + orchestrator/src/tools/coordination.ts | 32 +- orchestrator/src/tools/exec.ts | 22 + orchestrator/src/tools/manifest.ts | 35 + 24 files changed, 10354 insertions(+), 10 deletions(-) create mode 100644 orchestrator/drizzle/0105_mcp_binding_envelope.sql create mode 100644 orchestrator/drizzle/meta/0105_snapshot.json create mode 100644 orchestrator/src/__tests__/mcp-binding-fixture.ts create mode 100644 orchestrator/src/__tests__/mcp-binding-store.test.ts create mode 100644 orchestrator/src/__tests__/mcp-child-bindings.test.ts create mode 100644 orchestrator/src/__tests__/mcp-session-bindings.test.ts create mode 100644 orchestrator/src/__tests__/mcp-session-create.test.ts create mode 100644 orchestrator/src/__tests__/mcp-session-resolver.test.ts create mode 100644 orchestrator/src/mcp/binding-store.ts create mode 100644 orchestrator/src/mcp/session-bindings.ts create mode 100644 orchestrator/src/mcp/session-child.ts create mode 100644 orchestrator/src/mcp/session-create.ts create mode 100644 orchestrator/src/mcp/session-resolver.ts diff --git a/orchestrator/drizzle/0105_mcp_binding_envelope.sql b/orchestrator/drizzle/0105_mcp_binding_envelope.sql new file mode 100644 index 000000000..abcad55a8 --- /dev/null +++ b/orchestrator/drizzle/0105_mcp_binding_envelope.sql @@ -0,0 +1 @@ +ALTER TABLE "task_session" ADD COLUMN "mcp_binding_envelope" jsonb; diff --git a/orchestrator/drizzle/meta/0105_snapshot.json b/orchestrator/drizzle/meta/0105_snapshot.json new file mode 100644 index 000000000..15b79a8ea --- /dev/null +++ b/orchestrator/drizzle/meta/0105_snapshot.json @@ -0,0 +1,8439 @@ +{ + "id": "85dcfc01-8ed5-4cba-bd2d-709747d6a76a", + "prevId": "47c316d6-8319-40d1-b585-82c04b895dc9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_credential": { + "name": "mcp_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_binding_envelope": { + "name": "mcp_binding_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/_journal.json b/orchestrator/drizzle/meta/_journal.json index a63cd8f13..a19cbb1eb 100644 --- a/orchestrator/drizzle/meta/_journal.json +++ b/orchestrator/drizzle/meta/_journal.json @@ -736,6 +736,13 @@ "when": 1793585592276, "tag": "0104_mcp_credential_metadata", "breakpoints": true + }, + { + "idx": 105, + "version": "7", + "when": 1793602718404, + "tag": "0105_mcp_binding_envelope", + "breakpoints": true } ] } diff --git a/orchestrator/src/__tests__/connection-credential-broker.test.ts b/orchestrator/src/__tests__/connection-credential-broker.test.ts index 249ff08e6..8c9c51117 100644 --- a/orchestrator/src/__tests__/connection-credential-broker.test.ts +++ b/orchestrator/src/__tests__/connection-credential-broker.test.ts @@ -229,3 +229,46 @@ describe("named-connection credential broker", () => { expect(exchanges).toBe(0); }); }); + +test("mixed session audit hash includes the MCP binding envelope", async () => { + const { bindingAuthority } = await import("./mcp-binding-fixture.ts"); + const { compileSessionBindings } = await import("../mcp/session-bindings.ts"); + const { bindingSnapshotColumns } = await import("../mcp/binding-store.ts"); + const authority = bindingAuthority(); + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }); + const source = authority.connections[0]!; + const columns = bindingSnapshotColumns(snapshot, [ + { + id: source.id, + alias: source.alias, + displayName: "Docs", + provider: "mcp", + config: source.config, + }, + connection, + ]); + const session = { + userId: "user-1", + principalId: "user-1", + profileId: authority.profileId, + integrationGrants: sessionGrants, + ...columns, + }; + let auditHash = ""; + const app = makeConnectionCredentialBrokerRoute({ + db: {} as never, + bearer: "broker-secret", + sessions: { get: async () => session }, + sessionStatus: async () => "active", + exchange: async (_config, identity) => { + auditHash = identity.profileSnapshotId; + return { accessToken: "token", expiresAt: new Date(Date.now() + 60000) }; + }, + }); + expect((await app.request(path, post(body))).status).toBe(200); + expect(auditHash).toBe(integrationSnapshotHash(session)); +}); diff --git a/orchestrator/src/__tests__/mcp-binding-fixture.ts b/orchestrator/src/__tests__/mcp-binding-fixture.ts new file mode 100644 index 000000000..def8d210b --- /dev/null +++ b/orchestrator/src/__tests__/mcp-binding-fixture.ts @@ -0,0 +1,64 @@ +import { reviewCatalog } from "../mcp/catalog.ts"; +import type { ProfileIntegrationGrant } from "../db/schema.ts"; +export const schema = { + $schema: "https://json-schema.org/draft/2020-12/schema", + $defs: { name: { type: "string" } }, + type: "object", + properties: { name: { $ref: "#/$defs/name" } }, +}; +export const bindingGrant: ProfileIntegrationGrant = { + connectionId: "connection-a", + operation: "invoke", + credentialScope: "org", + resourceConstraints: [], + mcp: { + upstreamName: "Document.Search", + automationWrite: false, + unrestricted: true, + argumentConstraintDocuments: [], + }, +}; +export function bindingAuthority() { + return { + profileId: "profile-a", + profileGrants: [structuredClone(bindingGrant)], + connections: [ + { + id: "connection-a", + catalogId: "catalog-a-1", + alias: "docs", + enabled: true, + config: { + endpoint: "https://tools.example.test/mcp", + zone: "public", + auth: { kind: "api_key" as const, secretRef: "mcp.key" }, + }, + state: { + configRevision: 1, + authGeneration: 1, + catalogRevision: 1, + credentialIdentity: "identity-a", + revoked: false, + }, + tools: reviewCatalog([ + { + upstreamName: "Document.Search", + description: "Search", + inputSchema: schema, + classification: "read", + enabled: true, + }, + ]), + }, + ], + operatorPolicies: [ + { + zoneId: "public", + policyRevision: "1", + policyDigest: `sha256:${"a".repeat(64)}`, + authorities: ["tools.example.test"], + aliases: ["alias.example.test"], + }, + ], + }; +} diff --git a/orchestrator/src/__tests__/mcp-binding-store.test.ts b/orchestrator/src/__tests__/mcp-binding-store.test.ts new file mode 100644 index 000000000..23e71c989 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-binding-store.test.ts @@ -0,0 +1,174 @@ +import { expect, test } from "bun:test"; +import { eq } from "drizzle-orm"; +import { checkDb, getDb } from "../db/client.ts"; +import { task, taskSession } from "../db/schema.ts"; +import { compileSessionBindings } from "../mcp/session-bindings.ts"; +import { bindingSnapshotColumns, readStoredBindingSession } from "../mcp/binding-store.ts"; +import { bindingAuthority } from "./mcp-binding-fixture.ts"; +const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; +test.skipIf(!reachable)( + "binding envelope and existing connection snapshots round-trip atomically", + async () => { + const db = getDb(), + taskId = crypto.randomUUID(), + sessionId = crypto.randomUUID(); + const authority = bindingAuthority(); + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }); + const columns = bindingSnapshotColumns( + snapshot, + authority.connections.map((c) => ({ + id: c.id, + alias: c.alias, + provider: "mcp", + displayName: "Docs", + config: c.config, + })), + ); + try { + await db.insert(task).values({ id: taskId, type: "chat" }); + await expect( + db.transaction(async (tx) => { + await tx.insert(taskSession).values({ taskId, sessionId, ...columns }); + throw new Error("reservation rollback"); + }), + ).rejects.toThrow("reservation rollback"); + expect(await readStoredBindingSession(sessionId, db)).toBeNull(); + await db.insert(taskSession).values({ taskId, sessionId, ...columns }); + expect((await readStoredBindingSession(sessionId, db))?.snapshot).toEqual(snapshot); + expect(columns.mcpBindingEnvelope).not.toHaveProperty("immutableBindings"); + await db.update(task).set({ status: "cancelled" }).where(eq(task.id, taskId)); + expect((await readStoredBindingSession(sessionId, db))?.active).toBe(false); + await db + .update(taskSession) + .set({ integrationConnections: [] }) + .where(eq(taskSession.sessionId, sessionId)); + await expect(readStoredBindingSession(sessionId, db)).rejects.toThrow(); + } finally { + await db.delete(task).where(eq(task.id, taskId)); + } + }, +); +test.skipIf(!reachable)( + "resolver reads current stored profile and immutable published catalog authority", + async () => { + const { profile, integrationConnection, mcpCatalog } = await import("../db/schema.ts"); + const { readLiveBindingAuthority } = await import("../mcp/binding-store.ts"); + const { canonicalDigest } = await import("../mcp/hash.ts"); + const { checkBindingCurrent } = await import("../mcp/session-bindings.ts"); + const { integrationSnapshotHash } = await import("../integrations/grants.ts"); + const db = getDb(), + pid = crypto.randomUUID(), + cid = crypto.randomUUID(), + catalogId = crypto.randomUUID(); + const fixture = bindingAuthority(), + source = fixture.connections[0]!; + const grants = fixture.profileGrants.map((grant) => ({ ...grant, connectionId: cid })); + try { + await db + .insert(profile) + .values({ + id: pid, + name: "Binding test", + imageId: "image", + harness: "claude", + integrationGrants: grants, + }); + await db + .insert(integrationConnection) + .values({ + id: cid, + alias: `docs-${cid}`, + provider: "mcp", + displayName: "Docs", + config: source.config, + enabled: true, + ...source.state, + }); + await db + .insert(mcpCatalog) + .values({ + id: catalogId, + connectionId: cid, + kind: "published", + revision: 1, + configRevision: 1, + authGeneration: 1, + credentialIdentity: source.state.credentialIdentity, + tools: source.tools, + fingerprint: canonicalDigest(source.tools), + actorId: "test-operator", + createdAt: new Date(), + }); + const authority = await readLiveBindingAuthority(pid, fixture.operatorPolicies, db); + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: pid, integrationGrants: grants }, + liveAuthority: authority, + bindingEpoch: 0, + }); + expect(snapshot.immutableBindings[0]!.catalogId).toBe(catalogId); + const columns = bindingSnapshotColumns(snapshot, [ + { + id: cid, + alias: `docs-${cid}`, + provider: "mcp", + displayName: "Docs", + config: source.config, + }, + ]); + const fullHash = integrationSnapshotHash({ + profileId: pid, + integrationGrants: grants, + ...columns, + }); + expect(fullHash).not.toBe( + integrationSnapshotHash({ + profileId: pid, + integrationGrants: grants, + integrationConnections: columns.integrationConnections, + }), + ); + expect(fullHash).toBe( + integrationSnapshotHash( + JSON.parse(JSON.stringify({ profileId: pid, integrationGrants: grants, ...columns })), + ), + ); + // Credential refresh metadata changes alone do not alter the authority tuple. + await db + .update(integrationConnection) + .set({ testedAt: new Date(), updatedAt: new Date() }) + .where(eq(integrationConnection.id, cid)); + expect( + checkBindingCurrent({ + snapshot, + liveAuthority: await readLiveBindingAuthority(pid, fixture.operatorPolicies, db), + }).current, + ).toBe(true); + await db.update(profile).set({ integrationGrants: [] }).where(eq(profile.id, pid)); + expect( + checkBindingCurrent({ + snapshot, + liveAuthority: await readLiveBindingAuthority(pid, fixture.operatorPolicies, db), + }).current, + ).toBe(false); + await db.update(profile).set({ integrationGrants: grants }).where(eq(profile.id, pid)); + await db + .update(integrationConnection) + .set({ authGeneration: 2 }) + .where(eq(integrationConnection.id, cid)); + expect( + checkBindingCurrent({ + snapshot, + liveAuthority: await readLiveBindingAuthority(pid, fixture.operatorPolicies, db), + }).current, + ).toBe(false); + } finally { + await db.delete(mcpCatalog).where(eq(mcpCatalog.id, catalogId)); + await db.delete(integrationConnection).where(eq(integrationConnection.id, cid)); + await db.delete(profile).where(eq(profile.id, pid)); + } + }, +); diff --git a/orchestrator/src/__tests__/mcp-child-bindings.test.ts b/orchestrator/src/__tests__/mcp-child-bindings.test.ts new file mode 100644 index 000000000..bb1f8575a --- /dev/null +++ b/orchestrator/src/__tests__/mcp-child-bindings.test.ts @@ -0,0 +1,86 @@ +import { expect, test } from "bun:test"; +import { + compileChildSessionBindings, + assertChildMcpCreationBlocked, +} from "../mcp/session-child.ts"; +import { compileSessionBindings } from "../mcp/session-bindings.ts"; +import { bindingAuthority, bindingGrant } from "./mcp-binding-fixture.ts"; +import { checkDb, getDb } from "../db/client.ts"; +import { task, taskSession } from "../db/schema.ts"; +import { eq } from "drizzle-orm"; + +test("child compiler reads actual spawning session and defaults to no MCP request", async () => { + const parentAuthority = bindingAuthority(); + parentAuthority.profileGrants[0]!.mcp = { + ...bindingGrant.mcp!, + unrestricted: false, + argumentConstraintDocuments: [ + { + $schema: "http://json-schema.org/draft-07/schema#", + definitions: { p: { const: "parent" } }, + properties: { project: { $ref: "#/definitions/p" } }, + }, + ], + }; + const parent = compileSessionBindings({ + launchPolicy: { + profileId: parentAuthority.profileId, + integrationGrants: parentAuthority.profileGrants, + }, + liveAuthority: parentAuthority, + bindingEpoch: 0, + }); + const child = bindingAuthority(); + child.profileId = "child-profile"; + let selected = ""; + const args = { + spawningSessionId: "actual-parent-session", + launchPolicy: { profileId: child.profileId, integrationGrants: child.profileGrants }, + liveAuthority: child, + bindingEpoch: 0, + readParentCurrent: async (id: string) => { + selected = id; + return parent; + }, + }; + expect((await compileChildSessionBindings(args)).immutableBindings).toEqual([]); + const explicit = await compileChildSessionBindings({ ...args, requestedGrants: [bindingGrant] }); + expect(selected).toBe("actual-parent-session"); + expect(explicit.immutableBindings[0]!.tools[0]!.grant.argumentConstraintDocuments).toEqual( + parentAuthority.profileGrants[0]!.mcp!.argumentConstraintDocuments, + ); + await expect( + compileChildSessionBindings({ + ...args, + readParentCurrent: async () => { + throw new Error("parent revoked"); + }, + }), + ).rejects.toThrow("parent revoked"); +}); +const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; +test.skipIf(!reachable)( + "existing child path checks actual session authority even when launch policy is empty", + async () => { + const db = getDb(), + tid = crypto.randomUUID(), + sid = crypto.randomUUID(); + try { + await db.insert(task).values({ id: tid, type: "chat" }); + await db + .insert(taskSession) + .values({ taskId: tid, sessionId: sid, integrationGrants: [bindingGrant] }); + await expect(assertChildMcpCreationBlocked(tid, sid, [], db)).rejects.toThrow( + "activation is not configured", + ); + await db + .update(taskSession) + .set({ integrationGrants: [] }) + .where(eq(taskSession.sessionId, sid)); + await assertChildMcpCreationBlocked(tid, sid, [], db); + await expect(assertChildMcpCreationBlocked(tid, "other-session", [], db)).rejects.toThrow(); + } finally { + await db.delete(task).where(eq(task.id, tid)); + } + }, +); diff --git a/orchestrator/src/__tests__/mcp-session-bindings.test.ts b/orchestrator/src/__tests__/mcp-session-bindings.test.ts new file mode 100644 index 000000000..83a2b630d --- /dev/null +++ b/orchestrator/src/__tests__/mcp-session-bindings.test.ts @@ -0,0 +1,188 @@ +import { expect, test } from "bun:test"; +import { reviewCatalog } from "../mcp/catalog.ts"; +import { + compileSessionBindings, + checkBindingCurrent, + compileMcpManifest, +} from "../mcp/session-bindings.ts"; + +import { schema, bindingGrant, bindingAuthority } from "./mcp-binding-fixture.ts"; +function compile(authority = bindingAuthority()) { + return compileSessionBindings({ + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }); +} +test("session bindings preserve reviewed schema and freeze every authority input", () => { + const authority = bindingAuthority(); + const snapshot = compile(authority); + const manifest = compileMcpManifest(snapshot); + expect(manifest[0]?.inputSchema).toEqual(schema); + expect(manifest[0]?.execution).toBe("sync"); + expect(snapshot.managedDenyPolicy.hosts).toEqual(["alias.example.test", "tools.example.test"]); + const original = structuredClone(snapshot); + authority.connections[0]!.tools[0]!.inputSchema = { type: "null" }; + authority.operatorPolicies[0]!.aliases.push("new.example.test"); + expect(snapshot).toEqual(original); + expect(checkBindingCurrent({ snapshot, liveAuthority: authority }).current).toBe(false); +}); +test("all identity and authority changes invalidate; token refresh has no effect", () => { + const snapshot = compile(); + expect(checkBindingCurrent({ snapshot, liveAuthority: bindingAuthority() }).current).toBe(true); + const mutations = [ + (a: ReturnType) => { + a.connections[0]!.state.configRevision++; + }, + (a: ReturnType) => { + a.connections[0]!.state.authGeneration++; + }, + (a: ReturnType) => { + a.connections[0]!.state.catalogRevision++; + }, + (a: ReturnType) => { + a.connections[0]!.state.credentialIdentity = "identity-b"; + }, + (a: ReturnType) => { + a.connections[0]!.enabled = false; + }, + (a: ReturnType) => { + a.connections[0]!.state.revoked = true; + }, + (a: ReturnType) => { + a.connections[0]!.alias = "renamed"; + }, + (a: ReturnType) => { + a.connections[0]!.config.endpoint = "https://other.example.test/mcp"; + }, + (a: ReturnType) => { + a.operatorPolicies[0]!.policyRevision = "2"; + }, + (a: ReturnType) => { + a.operatorPolicies[0]!.policyDigest = `sha256:${"b".repeat(64)}`; + }, + (a: ReturnType) => { + a.operatorPolicies[0]!.aliases = []; + }, + (a: ReturnType) => { + a.profileGrants = []; + }, + ]; + for (const mutate of mutations) { + const authority = bindingAuthority(); + mutate(authority); + expect(checkBindingCurrent({ snapshot, liveAuthority: authority }).current).toBe(false); + } +}); +test("children intersect actual parent, profile and explicit request; empty never inherits", () => { + const authority = bindingAuthority(); + authority.profileGrants[0]!.mcp = { + ...bindingGrant.mcp!, + unrestricted: false, + argumentConstraintDocuments: [schema], + }; + const parent = compile(authority); + const childAuthority = bindingAuthority(); + childAuthority.profileGrants[0]!.mcp!.automationWrite = true; + const args = { + launchPolicy: { + profileId: childAuthority.profileId, + integrationGrants: childAuthority.profileGrants, + }, + liveAuthority: childAuthority, + bindingEpoch: 0, + parentAuthority: { sessionId: "parent", snapshot: parent }, + }; + const empty = compileSessionBindings({ ...args, requestedGrants: [] }); + expect(empty.immutableBindings).toEqual([]); + const child = compileSessionBindings({ ...args, requestedGrants: childAuthority.profileGrants }); + expect(child.immutableBindings[0]!.tools[0]!.grant.argumentConstraintDocuments).toEqual([schema]); + expect(child.immutableBindings[0]!.tools[0]!.grant.automationWrite).toBe(false); + expect(child.ancestorAuthorityRefs[0]?.bindingHash).toBe(parent.bindingHash); + expect(() => + compileSessionBindings({ + ...args, + requestedGrants: [ + { ...bindingGrant, mcp: { ...bindingGrant.mcp!, upstreamName: "Unknown" } }, + ], + }), + ).toThrow(); +}); +test("write descriptors stay deferred and catalog changes cannot mutate old manifest", () => { + const authority = bindingAuthority(); + authority.connections[0]!.tools = reviewCatalog([ + { + upstreamName: "Document.Search", + description: "Write", + inputSchema: schema, + classification: "write", + enabled: true, + }, + ]); + const snapshot = compile(authority); + const manifest = compileMcpManifest(snapshot); + expect(manifest[0]!.execution).toBe("deferred"); + manifest[0]!.inputSchema = {}; + expect(compileMcpManifest(snapshot)[0]!.inputSchema).toEqual(schema); +}); +test("missing operator approval, altered review fingerprints and over-limit requests fail closed", () => { + const authority = bindingAuthority(); + authority.operatorPolicies = []; + expect(() => compile(authority)).toThrow(); + const changed = bindingAuthority(); + changed.connections[0]!.tools[0]!.description = "Changed after review"; + expect(() => compile(changed)).toThrow(); + const tooMany = bindingAuthority(); + tooMany.connections = Array.from({ length: 17 }, (_, i) => ({ + ...tooMany.connections[0]!, + id: `c${i}`, + alias: `docs${i}`, + })); + tooMany.profileGrants = tooMany.connections.map((c) => ({ ...bindingGrant, connectionId: c.id })); + expect(() => compile(tooMany)).toThrow(); +}); +test("changes to managed destinations outside selected grants invalidate the deny set", () => { + const authority = bindingAuthority(); + authority.connections.push({ + ...structuredClone(authority.connections[0]!), + id: "ungranted", + alias: "other", + }); + authority.operatorPolicies[0]!.authorities.push("second.example.test"); + const snapshot = compile(authority); + authority.connections[1]!.config.endpoint = "https://second.example.test/mcp"; + expect(checkBindingCurrent({ snapshot, liveAuthority: authority }).current).toBe(false); +}); +test("binding freezes the immutable catalog row identifier", () => { + const authority = bindingAuthority(); + const snapshot = compile(authority); + expect(snapshot.immutableBindings[0]!.catalogId).toBe(authority.connections[0]!.catalogId); + authority.connections[0]!.catalogId = "replacement-catalog"; + expect(checkBindingCurrent({ snapshot, liveAuthority: authority }).current).toBe(false); +}); +test("merged manifest checks native collisions and the total tool and byte limits", async () => { + const { appendMcpManifest } = await import("../tools/manifest.ts"); + const snapshot = compile(); + const native = { + name: "native", + description: "Native", + inputSchema: { type: "object" }, + execution: "sync", + nativeBindings: {}, + }; + const limit = Array.from({ length: 127 }, (_, i) => ({ ...native, name: `native${i}` })); + expect(appendMcpManifest(JSON.stringify(limit), snapshot)).toHaveLength(128); + expect(() => appendMcpManifest(JSON.stringify([...limit, native]), snapshot)).toThrow(); + expect(() => + appendMcpManifest( + JSON.stringify([{ ...native, name: snapshot.immutableBindings[0]!.tools[0]!.name }]), + snapshot, + ), + ).toThrow(); + expect(() => + appendMcpManifest( + JSON.stringify([{ ...native, description: "a".repeat(1024 * 1024) }]), + snapshot, + ), + ).toThrow(); +}); diff --git a/orchestrator/src/__tests__/mcp-session-create.test.ts b/orchestrator/src/__tests__/mcp-session-create.test.ts new file mode 100644 index 000000000..80df391a6 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-session-create.test.ts @@ -0,0 +1,199 @@ +import { expect, test } from "bun:test"; +import { create } from "@bufbuild/protobuf"; +import { CreatePolicySessionResponseSchema } from "../gen/engram/mcp/v1/session_policy_pb.ts"; +import { makeBoundSessionCreator, compileBoundSessionCandidate } from "../mcp/session-create.ts"; +import { bindingAuthority } from "./mcp-binding-fixture.ts"; +import type { SessionPolicyControlClient } from "../mcp/control-client.ts"; + +test("closed creation adapter uses pinned policy creation and actual immutable parent", async () => { + const authority = bindingAuthority(); + const input = compileBoundSessionCandidate( + { imageUri: "image", mode: "agent", harness: "claude", requestedSessionId: "reserved" }, + { + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }, + ); + const calls: Parameters[0][] = []; + const creator = makeBoundSessionCreator({ + control: { + createPolicySession: async (req) => { + calls.push(req); + return create(CreatePolicySessionResponseSchema, { session: { sessionId: "reserved" } }); + }, + }, + readReservation: async () => ({ + principalId: "actor", + parentSessionId: null, + envelope: input.mcpBindingEnvelope!, + connections: input.integrationConnections!, + }), + authorizeInput: async () => {}, + readAuthority: async () => authority, + // Test-only trusted enforcement fixture; no production ACK implementation exists. + enforceManagedDeny: async (expected) => expected, + readParentCurrent: async () => { + throw new Error("no parent"); + }, + }); + expect( + (await creator.create(input, { actorId: "actor", inputId: "authenticated-input" })).sessionId, + ).toBe("reserved"); + expect(calls[0]?.protocol).toBe(1); + expect(calls[0]?.parentSessionId).toBeUndefined(); + expect(calls[0]?.inputId).toBe("authenticated-input"); + await expect( + creator.create(input, { actorId: "other", inputId: "authenticated-input" }), + ).rejects.toThrow(); + expect(calls.length).toBe(1); +}); +test("candidate does not expose tools; acknowledged creation preserves native tools", async () => { + const authority = bindingAuthority(); + const native = { + name: "native", + description: "Native", + inputSchema: { type: "object" }, + execution: "sync", + nativeBindings: {}, + }; + const input = compileBoundSessionCandidate( + { + imageUri: "image", + mode: "agent", + harness: "claude", + requestedSessionId: "reserved", + harnessEnv: { ENGRAM_TOOLS: JSON.stringify([native]) }, + }, + { + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }, + ); + expect(JSON.parse(input.harnessEnv!.ENGRAM_TOOLS!)).toEqual([native]); + let names: string[] = []; + const deps = { + control: { + createPolicySession: async ( + req: Parameters[0], + ) => { + names = JSON.parse(req.create!.harnessEnv!.ENGRAM_TOOLS!).map( + (t: { name: string }) => t.name, + ); + return create(CreatePolicySessionResponseSchema, { session: { sessionId: "reserved" } }); + }, + }, + readReservation: async () => ({ + principalId: "actor", + parentSessionId: null, + envelope: input.mcpBindingEnvelope!, + connections: input.integrationConnections!, + }), + authorizeInput: async () => {}, + readAuthority: async () => authority, + readParentCurrent: async () => { + throw new Error("no parent"); + }, + }; + await expect( + makeBoundSessionCreator(deps).create(input, { actorId: "actor", inputId: "input" }), + ).rejects.toThrow("enforcement unavailable"); + expect(names).toEqual([]); + await makeBoundSessionCreator({ + ...deps, + enforceManagedDeny: async (expected) => expected, + }).create(input, { actorId: "actor", inputId: "input" }); + expect(names).toContain("native"); + expect(names.length).toBe(2); +}); +test("reserved MCP creation can never fall through to generic session creation", async () => { + const { createReservedTaskSession } = await import("../mcp/session-create.ts"); + let genericCalls = 0; + const sessions = { + createSession: async () => { + genericCalls++; + return { sessionId: "reserved" }; + }, + }; + await expect( + createReservedTaskSession( + { sessions }, + { + imageUri: "image", + mode: "agent", + integrationGrants: [ + { + connectionId: "c", + operation: "invoke", + credentialScope: "org", + resourceConstraints: [], + mcp: { + upstreamName: "Tool", + unrestricted: true, + automationWrite: false, + argumentConstraintDocuments: [], + }, + }, + ], + }, + { actorId: "actor", inputId: "input" }, + ), + ).rejects.toThrow(); + expect(genericCalls).toBe(0); + await createReservedTaskSession( + { sessions }, + { imageUri: "image", mode: "agent" }, + { actorId: "actor", inputId: "input" }, + ); + expect(genericCalls).toBe(1); +}); + +test("clearing child harness mode cannot detach the stored parent policy", async () => { + const { compileSessionBindings } = await import("../mcp/session-bindings.ts"); + const authority = bindingAuthority(); + const compile = { + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }; + const parent = compileSessionBindings(compile); + const input = compileBoundSessionCandidate( + { + imageUri: "image", + mode: "agent", + harness: "claude", + requestedSessionId: "child", + harnessMode: undefined, + }, + { + ...compile, + parentAuthority: { sessionId: "actual-parent", snapshot: parent }, + requestedGrants: authority.profileGrants, + }, + ); + let receivedParent: string | undefined; + const creator = makeBoundSessionCreator({ + control: { + createPolicySession: async (req) => { + receivedParent = req.parentSessionId; + return create(CreatePolicySessionResponseSchema, { session: { sessionId: "child" } }); + }, + }, + readReservation: async () => ({ + principalId: "actor", + parentSessionId: "actual-parent", + envelope: input.mcpBindingEnvelope!, + connections: input.integrationConnections!, + }), + authorizeInput: async () => {}, + readAuthority: async () => authority, + readParentCurrent: async (id) => { + expect(id).toBe("actual-parent"); + return parent; + }, + enforceManagedDeny: async (expected) => expected, + }); + await creator.create(input, { actorId: "actor", inputId: "child-input" }); + expect(receivedParent).toBe("actual-parent"); +}); diff --git a/orchestrator/src/__tests__/mcp-session-resolver.test.ts b/orchestrator/src/__tests__/mcp-session-resolver.test.ts new file mode 100644 index 000000000..452f1f71b --- /dev/null +++ b/orchestrator/src/__tests__/mcp-session-resolver.test.ts @@ -0,0 +1,161 @@ +import { expect, test } from "bun:test"; +import { compileSessionBindings } from "../mcp/session-bindings.ts"; +import { + makeSessionBindingResolver, + type BoundSessionAuthority, + type SessionBindingResolverDeps, +} from "../mcp/session-resolver.ts"; +import { bindingAuthority, bindingGrant } from "./mcp-binding-fixture.ts"; + +function fixture() { + const authority = bindingAuthority(); + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }); + const rows = new Map([ + ["session-a", { snapshot, parentSessionId: null, active: true }], + ]); + const deps: SessionBindingResolverDeps = { + readSession: async (id) => rows.get(id) ?? null, + readAuthority: async () => authority, + readBindingEpoch: async () => 0, + // Explicit test adapter only. Production has no enforcement implementation. + enforceManagedDeny: async (expected) => expected, + }; + return { authority, snapshot, rows, deps }; +} +test("only exact names in this session resolve and results cannot mutate stored snapshots", async () => { + const f = fixture(); + const resolver = makeSessionBindingResolver(f.deps); + const name = f.snapshot.immutableBindings[0]!.tools[0]!.name; + const resolved = await resolver.resolveBoundTool({ sessionId: "session-a", modelToolName: name }); + expect(resolved?.bindingHash).toBe(f.snapshot.bindingHash); + resolved!.reviewedTool.inputSchema = {}; + expect( + (await resolver.resolveBoundTool({ sessionId: "session-a", modelToolName: name }))!.reviewedTool + .inputSchema, + ).not.toEqual({}); + for (const modelToolName of ["Document.Search", `${name}_spoof`, "mcp_docs_fake"]) + expect( + await resolver.resolveBoundTool({ sessionId: "session-a", modelToolName }), + ).toBeUndefined(); + expect( + await resolver.resolveBoundTool({ sessionId: "session-b", modelToolName: name }), + ).toBeUndefined(); +}); +test("absent ACK, wrong digest, restored epoch and changed ancestor grants deny", async () => { + const f = fixture(); + const childAuthority = bindingAuthority(); + childAuthority.profileId = "profile-child"; + const child = compileSessionBindings({ + launchPolicy: { + profileId: childAuthority.profileId, + integrationGrants: childAuthority.profileGrants, + }, + liveAuthority: childAuthority, + bindingEpoch: 0, + requestedGrants: [bindingGrant], + parentAuthority: { sessionId: "session-a", snapshot: f.snapshot }, + }); + f.rows.set("child", { snapshot: child, parentSessionId: "session-a", active: true }); + f.deps.readAuthority = async (id) => + id === childAuthority.profileId ? childAuthority : f.authority; + const input = { sessionId: "child", modelToolName: child.immutableBindings[0]!.tools[0]!.name }; + expect(await makeSessionBindingResolver(f.deps).resolveBoundTool(input)).toBeDefined(); + expect( + await makeSessionBindingResolver({ ...f.deps, enforceManagedDeny: undefined }).resolveBoundTool( + input, + ), + ).toBeUndefined(); + expect( + await makeSessionBindingResolver({ + ...f.deps, + enforceManagedDeny: async (expected) => ({ ...expected, denyDigest: "wrong" }), + }).resolveBoundTool(input), + ).toBeUndefined(); + expect( + await makeSessionBindingResolver({ + ...f.deps, + readBindingEpoch: async () => 1, + }).resolveBoundTool(input), + ).toBeUndefined(); + f.authority.profileGrants = []; + expect(await makeSessionBindingResolver(f.deps).resolveBoundTool(input)).toBeUndefined(); + f.authority.profileGrants = [bindingGrant]; + f.rows.get("child")!.parentSessionId = null; + expect(await makeSessionBindingResolver(f.deps).resolveBoundTool(input)).toBeUndefined(); +}); +test("two existing sessions resolve different catalogs without a global name registry", async () => { + const f = fixture(); + const other = bindingAuthority(); + other.profileId = "profile-b"; + other.connections[0]!.tools = []; + other.profileGrants = []; + const otherSnapshot = compileSessionBindings({ + launchPolicy: { profileId: other.profileId, integrationGrants: other.profileGrants }, + liveAuthority: other, + bindingEpoch: 0, + }); + f.rows.set("session-b", { snapshot: otherSnapshot, parentSessionId: null, active: true }); + f.deps.readAuthority = async (id) => (id === "profile-b" ? other : f.authority); + const resolver = makeSessionBindingResolver(f.deps), + name = f.snapshot.immutableBindings[0]!.tools[0]!.name; + expect( + await resolver.resolveBoundTool({ sessionId: "session-a", modelToolName: name }), + ).toBeDefined(); + expect( + await resolver.resolveBoundTool({ sessionId: "session-b", modelToolName: name }), + ).toBeUndefined(); +}); + +test("every ACK identity field and parent binding replacement is fenced", async () => { + const f = fixture(), + name = f.snapshot.immutableBindings[0]!.tools[0]!.name; + const input = { sessionId: "session-a", modelToolName: name }; + for (const field of ["sessionId", "bindingHash", "denyDigest"] as const) { + const deps = { + ...f.deps, + enforceManagedDeny: async ( + expected: import("../mcp/session-resolver.ts").ManagedDenyAck, + ) => ({ ...expected, [field]: "changed" }), + }; + expect(await makeSessionBindingResolver(deps).resolveBoundTool(input)).toBeUndefined(); + } + for (const field of ["bindingEpoch", "denyRevision"] as const) { + const deps = { + ...f.deps, + enforceManagedDeny: async ( + expected: import("../mcp/session-resolver.ts").ManagedDenyAck, + ) => ({ ...expected, [field]: 1 }), + }; + expect(await makeSessionBindingResolver(deps).resolveBoundTool(input)).toBeUndefined(); + } + const child = compileSessionBindings({ + launchPolicy: { + profileId: f.authority.profileId, + integrationGrants: f.authority.profileGrants, + }, + liveAuthority: f.authority, + bindingEpoch: 0, + requestedGrants: f.authority.profileGrants, + parentAuthority: { sessionId: "session-a", snapshot: f.snapshot }, + }); + f.rows.set("child", { snapshot: child, parentSessionId: "session-a", active: true }); + f.rows.get("session-a")!.snapshot = compileSessionBindings({ + launchPolicy: { + profileId: f.authority.profileId, + integrationGrants: f.authority.profileGrants, + }, + liveAuthority: f.authority, + bindingEpoch: 0, + denyRevision: 1, + }); + expect( + await makeSessionBindingResolver(f.deps).resolveBoundTool({ + sessionId: "child", + modelToolName: name, + }), + ).toBeUndefined(); +}); diff --git a/orchestrator/src/__tests__/tasks.test.ts b/orchestrator/src/__tests__/tasks.test.ts index b4475bbf4..11cd75479 100644 --- a/orchestrator/src/__tests__/tasks.test.ts +++ b/orchestrator/src/__tests__/tasks.test.ts @@ -754,6 +754,7 @@ function listSessionRef( integrationConnections: null, integrationPrincipalId: null, integrationSnapshotHash: null, + mcpBindingEnvelope: null, createdAt: new Date(createdAt), }; } diff --git a/orchestrator/src/db/schema.ts b/orchestrator/src/db/schema.ts index c5bd7c88d..47ceb3ef0 100644 --- a/orchestrator/src/db/schema.ts +++ b/orchestrator/src/db/schema.ts @@ -69,6 +69,7 @@ export interface TaskLaunchPolicy { integrationPolicyJson: string; integrationGrants: ProfileIntegrationGrant[]; integrationConnections: IntegrationConnectionSnapshot[]; + mcpBindingEnvelope?: import("../mcp/binding-store.ts").McpBindingEnvelope; network: ProfileNetwork; secrets: ProfileSecret[]; repos: ProfileRepo[]; @@ -173,6 +174,9 @@ export const taskSession = pgTable( // snapshot. This makes broker audit records attributable without consulting // mutable workflow state. integrationPrincipalId: text("integration_principal_id"), + // Candidate metadata only. The host must separately acknowledge deny enforcement. + mcpBindingEnvelope: jsonb("mcp_binding_envelope") + .$type(), // ADR 0109: content-hash of the compiled authorization snapshot // (profileId + grants + connections). The broker emits it as the // `engrams_profile_snapshot` OIDC claim, so Google-side audit logs join @@ -1176,7 +1180,7 @@ export interface ProfileRepo { /** Immutable non-secret connection configuration stamped onto a session. */ export interface IntegrationConnectionSnapshot { - mcp?: import("../mcp/types.ts").McpBinding; + mcp?: import("../mcp/session-bindings.ts").McpSessionBinding; id: string; alias: string; provider: string; diff --git a/orchestrator/src/integrations/grants.ts b/orchestrator/src/integrations/grants.ts index 38e1f353b..11218d057 100644 --- a/orchestrator/src/integrations/grants.ts +++ b/orchestrator/src/integrations/grants.ts @@ -23,6 +23,7 @@ export interface IntegrationSnapshot { profileId: string | null; integrationGrants: readonly ProfileIntegrationGrant[]; integrationConnections: readonly IntegrationConnectionSnapshot[]; + mcpBindingEnvelope?: import("../mcp/binding-store.ts").McpBindingEnvelope | null; } /** Recursively sort object keys so the hash survives a JSONB round-trip @@ -52,6 +53,7 @@ export function integrationSnapshotHash(snapshot: IntegrationSnapshot): string { profileId: snapshot.profileId, integrationGrants: snapshot.integrationGrants, integrationConnections: snapshot.integrationConnections, + ...(snapshot.mcpBindingEnvelope ? { mcpBindingEnvelope: snapshot.mcpBindingEnvelope } : {}), }))) .digest("hex"); return `sha256:${digest}`; diff --git a/orchestrator/src/mcp/binding-store.ts b/orchestrator/src/mcp/binding-store.ts new file mode 100644 index 000000000..119da0ee3 --- /dev/null +++ b/orchestrator/src/mcp/binding-store.ts @@ -0,0 +1,183 @@ +import { makeSessionBindingResolver } from "./session-resolver.ts"; +import { makeSessionWritePolicy } from "./session-policy.ts"; +import { and, eq } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { + task, + taskSession, + profile, + integrationConnection, + mcpCatalog, + type IntegrationConnectionSnapshot, +} from "../db/schema.ts"; +import { canonicalDigest } from "./hash.ts"; +import { reviewCatalog } from "./catalog.ts"; +import { validateMcpConfig } from "./config.ts"; +import type { BoundSessionAuthority } from "./session-resolver.ts"; +import type { + McpSessionBinding, + McpSessionBindingSnapshot, + McpOperatorPolicy, + McpLiveAuthority, +} from "./session-bindings.ts"; + +/** Bindings remain in integration_connections; this envelope contains only session provenance. */ +export type McpBindingEnvelope = Omit; +export function bindingSnapshotColumns( + snapshot: McpSessionBindingSnapshot, + connections: readonly IntegrationConnectionSnapshot[], +) { + const { immutableBindings, ...mcpBindingEnvelope } = structuredClone(snapshot); + const integrationConnections = connections + .map((connection) => { + const binding = immutableBindings.find((binding) => binding.connectionId === connection.id); + if (connection.provider === "mcp" && !binding) return null; + return { ...structuredClone(connection), ...(binding ? { mcp: binding } : {}) }; + }) + .filter((connection) => connection !== null); + if ( + integrationConnections.filter((connection) => connection.mcp).length !== + immutableBindings.length + ) + throw new Error("MCP snapshot connection identity missing"); + return { mcpBindingEnvelope, integrationConnections }; +} +export function restoreBindingSnapshot( + envelope: McpBindingEnvelope, + connections: readonly IntegrationConnectionSnapshot[], +): McpSessionBindingSnapshot { + const immutableBindings: McpSessionBinding[] = []; + for (const connection of connections) { + if (connection.provider !== "mcp") continue; + const binding = connection.mcp; + if ( + !binding || + !("alias" in binding) || + !("operatorPolicy" in binding) || + binding.connectionId !== connection.id || + binding.alias !== connection.alias || + canonicalDigest(binding.config) !== canonicalDigest(connection.config) + ) + throw new Error("MCP stored binding identity missing"); + immutableBindings.push(binding); + } + immutableBindings.sort((a, b) => + a.connectionId < b.connectionId ? -1 : a.connectionId > b.connectionId ? 1 : 0, + ); + const snapshot = { + ...structuredClone(envelope), + immutableBindings: structuredClone(immutableBindings), + }; + const { bindingHash, ...body } = snapshot; + if (canonicalDigest(body) !== bindingHash) throw new Error("MCP stored binding hash mismatch"); + return snapshot; +} +export async function readStoredBindingSession( + sessionId: string, + db = getDb(), +): Promise { + const rows = await db + .select({ + envelope: taskSession.mcpBindingEnvelope, + connections: taskSession.integrationConnections, + parentSessionId: task.spawningSessionId, + status: task.status, + }) + .from(taskSession) + .innerJoin(task, eq(task.id, taskSession.taskId)) + .where(eq(taskSession.sessionId, sessionId)) + .limit(2); + if (rows.length !== 1 || !rows[0]?.envelope) return null; + const row = rows[0]; + return { + snapshot: restoreBindingSnapshot(row.envelope!, row.connections ?? []), + parentSessionId: row.parentSessionId, + active: !["done", "failed", "cancelled"].includes(row.status), + }; +} + +/** Reads existing connection/catalog/profile authority. Operator policy has no admin-write path. */ +export async function readLiveBindingAuthority( + profileId: string, + operatorPolicies: readonly McpOperatorPolicy[], + db = getDb(), +): Promise { + const [currentProfile] = await db.select().from(profile).where(eq(profile.id, profileId)); + if (!currentProfile || currentProfile.deletedAt) throw new Error("MCP profile unavailable"); + const rows = await db + .select() + .from(integrationConnection) + .where(eq(integrationConnection.provider, "mcp")); + const connections: McpLiveAuthority["connections"] = []; + for (const row of rows) { + const [catalog] = await db + .select() + .from(mcpCatalog) + .where( + and( + eq(mcpCatalog.connectionId, row.id), + eq(mcpCatalog.kind, "published"), + eq(mcpCatalog.revision, row.catalogRevision), + ), + ); + const available = + catalog && + catalog.configRevision === row.configRevision && + catalog.authGeneration === row.authGeneration && + catalog.credentialIdentity === row.credentialIdentity; + const tools = available ? reviewCatalog(catalog.tools) : []; + if (available && canonicalDigest(tools) !== canonicalDigest(catalog.tools)) + throw new Error("MCP catalog fingerprint mismatch"); + connections.push({ + id: row.id, + catalogId: catalog?.id ?? "", + alias: row.alias, + config: validateMcpConfig(row.config), + enabled: row.enabled && Boolean(available) && !row.mcpCredential?.transition, + state: { + configRevision: row.configRevision, + authGeneration: row.authGeneration, + credentialIdentity: row.credentialIdentity, + catalogRevision: row.catalogRevision, + revoked: row.revoked, + }, + tools, + }); + } + return { + profileId, + profileGrants: currentProfile.integrationGrants, + connections, + operatorPolicies: structuredClone([...operatorPolicies]), + }; +} + +/** Production composition must supply both operator policy and the real host enforcement fence. */ +export function makeStoredSessionBindingResolver( + deps: { + control: import("./control-client.ts").SessionPolicyControlClient; + readOperatorPolicies(): Promise; + enforceManagedDeny?: import("./session-resolver.ts").SessionBindingResolverDeps["enforceManagedDeny"]; + }, + db = getDb(), +) { + const policy = makeSessionWritePolicy(deps.control); + return makeSessionBindingResolver({ + readSession: (sessionId) => readStoredBindingSession(sessionId, db), + readAuthority: async (profileId) => + readLiveBindingAuthority(profileId, await deps.readOperatorPolicies(), db), + readBindingEpoch: async (sessionId) => { + const current = await policy.readForRequest({ sessionId }); + const epoch = current.ref?.bindingEpoch; + if ( + !current.initialized || + epoch === undefined || + epoch < 0n || + epoch > BigInt(Number.MAX_SAFE_INTEGER) + ) + throw new Error("MCP current binding epoch unavailable"); + return Number(epoch); + }, + ...(deps.enforceManagedDeny ? { enforceManagedDeny: deps.enforceManagedDeny } : {}), + }); +} diff --git a/orchestrator/src/mcp/session-bindings.ts b/orchestrator/src/mcp/session-bindings.ts new file mode 100644 index 000000000..6bc1b9c13 --- /dev/null +++ b/orchestrator/src/mcp/session-bindings.ts @@ -0,0 +1,323 @@ +import { isIP } from "node:net"; +import type { ProfileIntegrationGrant } from "../db/schema.ts"; +import type { ToolManifestEntry } from "../tools/manifest.ts"; +import { compileBindings, intersectMcpGrants } from "./bindings.ts"; +import { reviewCatalog } from "./catalog.ts"; +import { validateMcpConfig } from "./config.ts"; +import { canonicalDigest, canonicalJson } from "./hash.ts"; +import { MCP_LIMITS } from "./limits.ts"; +import type { McpBinding, McpConnectionState, McpReviewedTool } from "./types.ts"; + +/** Operator-owned projection of the worker's approved zone policy. Never profile input. */ +export interface McpOperatorPolicy { + zoneId: string; + policyRevision: string; + policyDigest: string; + authorities: string[]; + aliases: string[]; +} +export interface McpLiveAuthority { + profileId: string; + profileGrants: ProfileIntegrationGrant[]; + connections: { + id: string; + catalogId: string; + alias: string; + enabled: boolean; + config: unknown; + state: McpConnectionState; + tools: McpReviewedTool[]; + }[]; + operatorPolicies: McpOperatorPolicy[]; +} +export interface McpSessionBinding extends McpBinding { + catalogId: string; + alias: string; + operatorPolicy: McpOperatorPolicy; +} +export interface McpAncestorAuthorityRef { + sessionId: string; + bindingHash: string; + bindingEpoch: number; +} +export interface ManagedDenyPolicy { + version: 1; + revision: number; + hosts: string[]; + digest: string; +} +export interface McpSessionBindingSnapshot { + version: 1; + bindingEpoch: number; + profileId: string; + profileGrantsHash: string; + managedAuthorityHash: string; + immutableBindings: McpSessionBinding[]; + ancestorAuthorityRefs: McpAncestorAuthorityRef[]; + managedDenyPolicy: ManagedDenyPolicy; + bindingHash: string; +} +/** Supplied only from the actual parent resolver after its full currentness check. */ +export interface McpParentAuthority { + sessionId: string; + snapshot: McpSessionBindingSnapshot; +} + +function mcpGrants(grants: readonly ProfileIntegrationGrant[]) { + return grants.filter((grant) => grant.mcp !== undefined); +} +function profileHash(grants: readonly ProfileIntegrationGrant[]) { + return canonicalDigest(mcpGrants(grants)); +} +function normalizedPolicy(policy: McpOperatorPolicy): McpOperatorPolicy { + if ( + !policy.zoneId || + !policy.policyRevision || + !/^sha256:[0-9a-f]{64}$/.test(policy.policyDigest) + ) + throw new Error("operator MCP policy identity unavailable"); + const authorities = [...new Set(policy.authorities)].sort(); + const aliases = [...new Set(policy.aliases)].sort(); + for (const authority of authorities) { + const url = new URL(`https://${authority}/`); + if (url.host !== authority || url.username || url.password || url.pathname !== "/") + throw new Error("operator authority must be canonical"); + assertHostname(url.hostname); + } + for (const alias of aliases) assertHostname(alias); + return { + zoneId: policy.zoneId, + policyRevision: policy.policyRevision, + policyDigest: policy.policyDigest, + authorities, + aliases, + }; +} +function assertHostname(host: string) { + if ( + host.length > 253 || + isIP(host) || + !host.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) + ) + throw new Error("operator hostname must be canonical"); +} +function policyFor(config: ReturnType, live: McpLiveAuthority) { + const matches = live.operatorPolicies.filter((policy) => policy.zoneId === config.zone); + if (matches.length !== 1) throw new Error("operator MCP zone policy unavailable"); + const policy = normalizedPolicy(matches[0]!); + if (!policy.authorities.includes(new URL(config.endpoint).host)) + throw new Error("MCP authority is not operator approved"); + return policy; +} +function managedAuthorityHash(live: McpLiveAuthority) { + return canonicalDigest( + live.connections + .map((connection) => { + const config = validateMcpConfig(connection.config); + return { + id: connection.id, + endpoint: config.endpoint, + operatorPolicy: policyFor(config, live), + }; + }) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), + ); +} +function effectiveGrants(snapshot: McpSessionBindingSnapshot): ProfileIntegrationGrant[] { + return snapshot.immutableBindings.flatMap((binding) => + binding.tools.map((tool) => ({ + connectionId: binding.connectionId, + operation: "invoke", + credentialScope: "org" as const, + resourceConstraints: [], + mcp: structuredClone(tool.grant), + })), + ); +} +function assertSnapshot(snapshot: McpSessionBindingSnapshot) { + const { bindingHash, ...body } = snapshot; + const { digest, ...deny } = snapshot.managedDenyPolicy; + if ( + snapshot.version !== 1 || + canonicalDigest(body) !== bindingHash || + canonicalDigest(deny) !== digest + ) + throw new Error("MCP snapshot integrity failed"); +} + +/** Compile a candidate only. This does not install or acknowledge guest denies. */ +export function compileSessionBindings(input: { + launchPolicy: { profileId: string; integrationGrants: readonly ProfileIntegrationGrant[] }; + liveAuthority: McpLiveAuthority; + bindingEpoch: number; + /** New sessions start at zero. Rebind must supply its monotonic policy revision. */ + denyRevision?: number; + requestedGrants?: readonly ProfileIntegrationGrant[]; + parentAuthority?: McpParentAuthority; +}): McpSessionBindingSnapshot { + const { launchPolicy, liveAuthority: live, parentAuthority: parent } = input; + const denyRevision = input.denyRevision ?? 0; + if (!Number.isSafeInteger(denyRevision) || denyRevision < 0) + throw new Error("invalid managed deny revision"); + if ( + !Number.isSafeInteger(input.bindingEpoch) || + input.bindingEpoch < 0 || + launchPolicy.profileId !== live.profileId + ) + throw new Error("MCP binding context unavailable"); + if (profileHash(launchPolicy.integrationGrants) !== profileHash(live.profileGrants)) + throw new Error("MCP profile changed during compilation"); + let grants = mcpGrants(launchPolicy.integrationGrants); + if (input.requestedGrants !== undefined) + grants = intersectMcpGrants(grants, mcpGrants(input.requestedGrants)); + if (parent) { + assertSnapshot(parent.snapshot); + // No request means no child tools. A parent snapshot is never an implicit grant. + grants = intersectMcpGrants( + effectiveGrants(parent.snapshot), + input.requestedGrants === undefined ? [] : grants, + ); + } + const sources = [...new Set(grants.map((grant) => grant.connectionId))].sort().map((id) => { + const matches = live.connections.filter((connection) => connection.id === id); + if (matches.length !== 1) throw new Error("MCP connection authority unavailable"); + const source = matches[0]!; + if (!source.catalogId) throw new Error("MCP catalog identity unavailable"); + if (canonicalDigest(reviewCatalog(source.tools)) !== canonicalDigest(source.tools)) + throw new Error("MCP catalog review fingerprint changed"); + return { ...source, grants: grants.filter((grant) => grant.connectionId === id) }; + }); + const immutableBindings = compileBindings(sources).map((binding) => { + const source = sources.find((source) => source.id === binding.connectionId)!; + const extended = { + ...binding, + catalogId: source.catalogId, + alias: source.alias, + operatorPolicy: policyFor(binding.config, live), + }; + const { hash: _hash, ...body } = extended; + return { ...body, hash: canonicalDigest(body) }; + }); + // Keep managed destinations even when the request has no tool grants. + const hosts = new Set(parent?.snapshot.managedDenyPolicy.hosts ?? []); + for (const connection of live.connections) { + const config = validateMcpConfig(connection.config); + const policy = policyFor(config, live); + hosts.add(new URL(config.endpoint).hostname); + for (const alias of policy.aliases) hosts.add(alias); + } + const deny = { version: 1 as const, revision: denyRevision, hosts: [...hosts].sort() }; + const ancestorAuthorityRefs = parent + ? [ + { + sessionId: parent.sessionId, + bindingHash: parent.snapshot.bindingHash, + bindingEpoch: parent.snapshot.bindingEpoch, + }, + ...parent.snapshot.ancestorAuthorityRefs, + ] + : []; + if ( + ancestorAuthorityRefs.length >= 32 || + new Set(ancestorAuthorityRefs.map((ref) => ref.sessionId)).size !== ancestorAuthorityRefs.length + ) + throw new Error("invalid MCP ancestry"); + const body = { + version: 1 as const, + bindingEpoch: input.bindingEpoch, + profileId: live.profileId, + profileGrantsHash: profileHash(live.profileGrants), + immutableBindings, + ancestorAuthorityRefs, + managedAuthorityHash: managedAuthorityHash(live), + managedDenyPolicy: { ...deny, digest: canonicalDigest(deny) }, + }; + const snapshot = structuredClone({ ...body, bindingHash: canonicalDigest(body) }); + compileMcpManifest(snapshot); + return snapshot; +} + +/** Exact identity checks. Token bytes/expiry are deliberately absent from this authority. */ +export function checkBindingCurrent(input: { + snapshot: McpSessionBindingSnapshot; + liveAuthority: McpLiveAuthority; +}): { current: true } | { current: false; staleReasons: string[] } { + const staleReasons: string[] = []; + const { snapshot, liveAuthority: live } = input; + try { + assertSnapshot(snapshot); + } catch { + staleReasons.push("snapshot_integrity"); + } + if ( + snapshot.profileId !== live.profileId || + snapshot.profileGrantsHash !== profileHash(live.profileGrants) + ) + staleReasons.push("profile_grants"); + try { + if (snapshot.managedAuthorityHash !== managedAuthorityHash(live)) + staleReasons.push("managed_destinations"); + } catch { + staleReasons.push("managed_destinations_unavailable"); + } + for (const binding of snapshot.immutableBindings) { + const matches = live.connections.filter((connection) => connection.id === binding.connectionId); + const connection = matches[0]; + if (matches.length !== 1 || !connection || !connection.enabled || connection.state.revoked) { + staleReasons.push("connection_unavailable"); + continue; + } + for (const field of [ + "configRevision", + "authGeneration", + "credentialIdentity", + "catalogRevision", + ] as const) + if (binding[field] !== connection.state[field]) staleReasons.push(field); + if (binding.catalogId !== connection.catalogId) staleReasons.push("catalog_identity"); + if ( + binding.alias !== connection.alias || + canonicalDigest(binding.config) !== canonicalDigest(connection.config) + ) + staleReasons.push("connection_identity"); + try { + if ( + canonicalDigest(binding.operatorPolicy) !== canonicalDigest(policyFor(binding.config, live)) + ) + staleReasons.push("operator_policy"); + const reviewed = reviewCatalog(connection.tools); + if (canonicalDigest(reviewed) !== canonicalDigest(connection.tools)) + staleReasons.push("catalog_fingerprint"); + for (const bound of binding.tools) { + const tool = reviewed.find((tool) => tool.upstreamName === bound.tool.upstreamName); + if (!tool?.enabled || canonicalDigest(tool) !== canonicalDigest(bound.tool)) + staleReasons.push("reviewed_tool"); + } + } catch { + staleReasons.push("operator_or_catalog_unavailable"); + } + } + return staleReasons.length + ? { current: false, staleReasons: [...new Set(staleReasons)] } + : { current: true }; +} + +/** Raw JSON Schema stays in its original dialect and resource scope. */ +export function compileMcpManifest(snapshot: McpSessionBindingSnapshot): ToolManifestEntry[] { + assertSnapshot(snapshot); + const manifest = snapshot.immutableBindings.flatMap((binding) => + binding.tools.map((bound) => ({ + name: bound.name, + description: bound.tool.description, + inputSchema: structuredClone(bound.tool.inputSchema), + execution: bound.tool.classification === "write" ? ("deferred" as const) : ("sync" as const), + nativeBindings: {}, + })), + ); + if ( + manifest.length > MCP_LIMITS.toolsPerSession || + Buffer.byteLength(canonicalJson(manifest)) > MCP_LIMITS.manifestBytes + ) + throw new Error("MCP manifest exceeds session limits"); + return manifest; +} diff --git a/orchestrator/src/mcp/session-child.ts b/orchestrator/src/mcp/session-child.ts new file mode 100644 index 000000000..a79869ab2 --- /dev/null +++ b/orchestrator/src/mcp/session-child.ts @@ -0,0 +1,42 @@ +import { and, eq } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { taskSession, type ProfileIntegrationGrant } from "../db/schema.ts"; +import { compileSessionBindings, type McpSessionBindingSnapshot } from "./session-bindings.ts"; + +/** The caller supplies the actual tool-call session ID, never a task launch snapshot. */ +export async function compileChildSessionBindings( + input: Omit[0], "parentAuthority"> & { + spawningSessionId: string; + readParentCurrent(sessionId: string): Promise; + }, +) { + const snapshot = await input.readParentCurrent(input.spawningSessionId); + return compileSessionBindings({ + ...input, + requestedGrants: input.requestedGrants ?? [], + parentAuthority: { sessionId: input.spawningSessionId, snapshot }, + }); +} +/** Retain the unconditional activation guard until the host can enforce child denies. */ +export async function assertChildMcpCreationBlocked( + taskId: string, + spawningSessionId: string, + launchGrants: readonly ProfileIntegrationGrant[], + db = getDb(), +) { + const [parent] = await db + .select({ + grants: taskSession.integrationGrants, + connections: taskSession.integrationConnections, + envelope: taskSession.mcpBindingEnvelope, + }) + .from(taskSession) + .where(and(eq(taskSession.taskId, taskId), eq(taskSession.sessionId, spawningSessionId))); + if (!parent) throw new Error("spawning session authority unavailable"); + if ( + parent.envelope || + [...(parent.grants ?? []), ...launchGrants].some((grant) => grant.mcp) || + parent.connections?.some((connection) => connection.provider === "mcp") + ) + throw new Error("external MCP session activation is not configured"); +} diff --git a/orchestrator/src/mcp/session-create.ts b/orchestrator/src/mcp/session-create.ts new file mode 100644 index 000000000..8d75ae4d8 --- /dev/null +++ b/orchestrator/src/mcp/session-create.ts @@ -0,0 +1,170 @@ +import { appendMcpManifest } from "../tools/manifest.ts"; +import type { IntegrationConnectionSnapshot } from "../db/schema.ts"; +import type { SessionCreateInput } from "../rpc/task-create.ts"; +import type { SessionPolicyControlClient } from "./control-client.ts"; +import { + bindingSnapshotColumns, + restoreBindingSnapshot, + type McpBindingEnvelope, +} from "./binding-store.ts"; +import { + checkBindingCurrent, + compileSessionBindings, + type McpLiveAuthority, + type McpSessionBindingSnapshot, + type ManagedDenyPolicy, +} from "./session-bindings.ts"; +import { + assertManagedDenyAck, + expectedManagedDenyAck, + type ManagedDenyAck, +} from "./session-resolver.ts"; + +/** Candidate preparation is separate from activation. It cannot put tools into guest env. */ +export function compileBoundSessionCandidate( + input: SessionCreateInput, + authority: Parameters[0], +): SessionCreateInput { + const snapshot = compileSessionBindings(authority); + const connections: IntegrationConnectionSnapshot[] = authority.liveAuthority.connections.map( + (connection) => ({ + id: connection.id, + alias: connection.alias, + displayName: connection.alias, + provider: "mcp", + config: structuredClone( + snapshot.immutableBindings.find((binding) => binding.connectionId === connection.id) + ?.config ?? {}, + ), + }), + ); + const direct = (input.integrationConnections ?? []).filter( + (connection) => connection.provider !== "mcp", + ); + const grants = snapshot.immutableBindings.flatMap((binding) => + binding.tools.map((bound) => ({ + connectionId: binding.connectionId, + operation: "invoke", + credentialScope: "org" as const, + resourceConstraints: [], + mcp: structuredClone(bound.grant), + })), + ); + return { + ...input, + ...bindingSnapshotColumns(snapshot, [...direct, ...connections]), + integrationGrants: [ + ...(input.integrationGrants ?? []).filter((grant) => !grant.mcp), + ...grants, + ], + }; +} +export interface BoundSessionReservation { + principalId: string | null; + parentSessionId: string | null; + envelope: McpBindingEnvelope; + connections: IntegrationConnectionSnapshot[]; +} +export interface TrustedCreationInput { + actorId: string; + inputId: string; +} +export interface BoundSessionCreatorDeps { + control: Pick; + readReservation(sessionId: string): Promise; + /** Check the current authenticated source input, principal and permission; never guest metadata. */ + authorizeInput(input: TrustedCreationInput, sessionId: string): Promise; + readAuthority(profileId: string): Promise; + readParentCurrent(sessionId: string): Promise; + enforceManagedDeny?(expected: ManagedDenyAck, policy: ManagedDenyPolicy): Promise; +} +/** Closed adapter for Task 6 composition. Generic create never initializes write authority. */ +export function makeBoundSessionCreator(deps: BoundSessionCreatorDeps) { + return { + async create(input: SessionCreateInput, source: TrustedCreationInput) { + const sessionId = input.requestedSessionId; + if (!sessionId || !source.actorId || !source.inputId || !input.mcpBindingEnvelope) + throw new Error("trusted MCP creation context required"); + await deps.authorizeInput(source, sessionId); + const reservation = await deps.readReservation(sessionId); + if (!reservation || reservation.principalId !== source.actorId) + throw new Error("MCP reservation principal changed"); + const snapshot = restoreBindingSnapshot(reservation.envelope, reservation.connections); + const candidate = restoreBindingSnapshot( + input.mcpBindingEnvelope, + input.integrationConnections ?? [], + ); + if (candidate.bindingHash !== snapshot.bindingHash || snapshot.bindingEpoch !== 0) + throw new Error("MCP creation binding changed"); + if ( + !checkBindingCurrent({ + snapshot, + liveAuthority: await deps.readAuthority(snapshot.profileId), + }).current + ) + throw new Error("MCP creation authority changed"); + const parentId = reservation.parentSessionId; + if (parentId) { + const parent = await deps.readParentCurrent(parentId); + const expected = snapshot.ancestorAuthorityRefs[0]; + if ( + !expected || + expected.sessionId !== parentId || + expected.bindingHash !== parent.bindingHash || + expected.bindingEpoch !== parent.bindingEpoch + ) + throw new Error("MCP creation parent changed"); + } else if (snapshot.ancestorAuthorityRefs.length) + throw new Error("MCP creation parent missing"); + if (!deps.enforceManagedDeny) throw new Error("MCP managed deny enforcement unavailable"); + const expected = expectedManagedDenyAck(sessionId, snapshot); + assertManagedDenyAck( + expected, + await deps.enforceManagedDeny(expected, snapshot.managedDenyPolicy), + ); + const { + mcpBindingEnvelope: _envelope, + integrationConnections: _connections, + integrationGrants: _grants, + ...wire + } = input; + const response = await deps.control.createPolicySession({ + protocol: 1, + actorId: source.actorId, + inputId: source.inputId, + ...(parentId ? { parentSessionId: parentId } : {}), + create: { + ...wire, + harnessEnv: { + ...wire.harnessEnv, + ENGRAM_TOOLS: JSON.stringify( + appendMcpManifest(wire.harnessEnv?.ENGRAM_TOOLS, snapshot), + ), + }, + }, + }); + if (response.session?.sessionId !== sessionId) + throw new Error("MCP policy session response mismatch"); + return response.session; + }, + }; +} + +/** The normal task path cannot route MCP authority through legacy/generic creation. */ +export async function createReservedTaskSession( + deps: { + sessions: { createSession(input: SessionCreateInput): Promise<{ sessionId: string }> }; + boundSessionCreator?: ReturnType; + }, + input: SessionCreateInput, + source: TrustedCreationInput, +) { + const hasMcp = + input.mcpBindingEnvelope || + input.integrationGrants?.some((grant) => grant.mcp) || + input.integrationConnections?.some((connection) => connection.provider === "mcp"); + if (!hasMcp) return deps.sessions.createSession(input); + if (!input.mcpBindingEnvelope || !deps.boundSessionCreator) + throw new Error("external MCP session activation is not configured"); + return deps.boundSessionCreator.create(input, source); +} diff --git a/orchestrator/src/mcp/session-resolver.ts b/orchestrator/src/mcp/session-resolver.ts new file mode 100644 index 000000000..13c6a31bb --- /dev/null +++ b/orchestrator/src/mcp/session-resolver.ts @@ -0,0 +1,134 @@ +import type { McpBoundTool } from "./types.ts"; +import { + checkBindingCurrent, + type McpLiveAuthority, + type McpSessionBindingSnapshot, + type McpSessionBinding, + type McpAncestorAuthorityRef, + type ManagedDenyPolicy, +} from "./session-bindings.ts"; + +/** Task 6 must obtain this from the trusted host after durable apply and connection closure. + * A stored receipt, echo, or registry replacement is not an enforcement implementation. */ +export interface ManagedDenyAck { + sessionId: string; + bindingEpoch: number; + bindingHash: string; + denyRevision: number; + denyDigest: string; +} +export function expectedManagedDenyAck( + sessionId: string, + snapshot: McpSessionBindingSnapshot, +): ManagedDenyAck { + return { + sessionId, + bindingEpoch: snapshot.bindingEpoch, + bindingHash: snapshot.bindingHash, + denyRevision: snapshot.managedDenyPolicy.revision, + denyDigest: snapshot.managedDenyPolicy.digest, + }; +} +export function assertManagedDenyAck(expected: ManagedDenyAck, actual: ManagedDenyAck) { + if ( + Object.keys(expected).some( + (key) => expected[key as keyof ManagedDenyAck] !== actual[key as keyof ManagedDenyAck], + ) + ) + throw new Error("MCP managed deny enforcement changed"); +} +export interface BoundSessionAuthority { + snapshot: McpSessionBindingSnapshot; + parentSessionId: string | null; + active: boolean; +} +export interface SessionBindingResolverDeps { + readSession(sessionId: string): Promise; + readAuthority(profileId: string): Promise; + /** Fresh coordinator policy reference. Unknown/legacy/terminal sessions must reject. */ + readBindingEpoch(sessionId: string): Promise; + /** Mandatory trusted host read/apply fence. Do not implement with a stored JSON receipt. */ + enforceManagedDeny?(expected: ManagedDenyAck, policy: ManagedDenyPolicy): Promise; +} +export interface ResolvedBoundTool { + bindingHash: string; + binding: McpSessionBinding; + modelToolName: string; + reviewedTool: McpBoundTool["tool"]; + effectiveGrant: McpBoundTool["grant"]; + ancestorAuthorityRefs: McpAncestorAuthorityRef[]; + execution: "sync" | "deferred"; +} +/** Session-local resolution never changes the process-wide registry or accepts a destination. */ +export function makeSessionBindingResolver(deps: SessionBindingResolverDeps) { + async function readCurrent(sessionId: string): Promise { + const leaf = await deps.readSession(sessionId); + if (!leaf?.active) throw new Error("MCP session unavailable"); + const refs = leaf.snapshot.ancestorAuthorityRefs; + if (refs.length >= 32) throw new Error("MCP ancestry unavailable"); + const seen = new Set(); + let id: string | null = sessionId; + for (let index = 0; index <= refs.length; index++) { + if (!id || seen.has(id)) throw new Error("MCP ancestry unavailable"); + seen.add(id); + const row: BoundSessionAuthority | null = index === 0 ? leaf : await deps.readSession(id); + if (!row?.active) throw new Error("MCP ancestor unavailable"); + const snapshot = row.snapshot; + const expected = index === 0 ? undefined : refs[index - 1]; + if ( + expected && + (expected.sessionId !== id || + expected.bindingHash !== snapshot.bindingHash || + expected.bindingEpoch !== snapshot.bindingEpoch) + ) + throw new Error("MCP ancestor binding changed"); + const suffix = refs.slice(index); + if (JSON.stringify(snapshot.ancestorAuthorityRefs) !== JSON.stringify(suffix)) + throw new Error("MCP ancestry changed"); + const live = await deps.readAuthority(snapshot.profileId); + if ( + !checkBindingCurrent({ snapshot, liveAuthority: live }).current || + (await deps.readBindingEpoch(id)) !== snapshot.bindingEpoch + ) + throw new Error("MCP authority changed"); + id = row.parentSessionId; + } + if (id !== null) throw new Error("MCP ancestry truncated"); + if (!deps.enforceManagedDeny) throw new Error("MCP managed deny enforcement unavailable"); + const expected = expectedManagedDenyAck(sessionId, leaf.snapshot); + assertManagedDenyAck( + expected, + await deps.enforceManagedDeny(expected, leaf.snapshot.managedDenyPolicy), + ); + return leaf.snapshot; + } + return { + readCurrent, + async resolveBoundTool(input: { + sessionId: string; + modelToolName: string; + }): Promise { + try { + const snapshot = await readCurrent(input.sessionId); + for (const binding of snapshot.immutableBindings) { + const bound = binding.tools.find((tool) => tool.name === input.modelToolName); + if (bound) + return structuredClone({ + bindingHash: snapshot.bindingHash, + binding, + modelToolName: bound.name, + reviewedTool: bound.tool, + effectiveGrant: bound.grant, + ancestorAuthorityRefs: snapshot.ancestorAuthorityRefs, + execution: bound.tool.classification === "write" ? "deferred" : "sync", + }); + } + } catch { + /* Missing authority, enforcement and malformed old snapshots all deny. */ + } + return undefined; + }, + }; +} + +export type SessionBindingResolver = ReturnType; diff --git a/orchestrator/src/routes/connection-credential-broker.ts b/orchestrator/src/routes/connection-credential-broker.ts index 87825032b..bca8122bc 100644 --- a/orchestrator/src/routes/connection-credential-broker.ts +++ b/orchestrator/src/routes/connection-credential-broker.ts @@ -54,6 +54,7 @@ interface CachedToken { } export interface CredentialBrokerSession { + mcpBindingEnvelope?: import("../mcp/binding-store.ts").McpBindingEnvelope | null; userId: string | null; principalId: string | null; profileId: string | null; @@ -135,6 +136,7 @@ export function makeConnectionCredentialBrokerRoute(deps: { profileId: taskSession.profileId, integrationGrants: taskSession.integrationGrants, integrationConnections: taskSession.integrationConnections, + mcpBindingEnvelope: taskSession.mcpBindingEnvelope, }).from(taskSession).innerJoin(task, eq(taskSession.taskId, task.id)).where( eq(taskSession.sessionId, sessionId), ).limit(1); @@ -178,6 +180,7 @@ export function makeConnectionCredentialBrokerRoute(deps: { profileId: session.profileId, integrationGrants: grants, integrationConnections: connections, + mcpBindingEnvelope: session.mcpBindingEnvelope, }); let outcome = "denied"; diff --git a/orchestrator/src/rpc/task-create.ts b/orchestrator/src/rpc/task-create.ts index 86d958bd4..e96c0f5d0 100644 --- a/orchestrator/src/rpc/task-create.ts +++ b/orchestrator/src/rpc/task-create.ts @@ -17,6 +17,8 @@ * policy. */ +import { createReservedTaskSession, type makeBoundSessionCreator } from "../mcp/session-create.ts"; + import { resolveIntegrationHost } from "../integrations/run-op.ts"; import { ConnectError, Code } from "@connectrpc/connect"; import { and, eq, isNull } from "drizzle-orm"; @@ -116,6 +118,8 @@ export type Db = NodePgDatabase; /** The control-plane CreateSession request the orchestrator compiles. Mirrors * the `SessionsClient.createSession` request shape (rpc/tasks.ts). */ export interface SessionCreateInput { + /** Candidate metadata only; production MCP creation remains guarded. */ + mcpBindingEnvelope?: import("../mcp/binding-store.ts").McpBindingEnvelope; /** ADR 0109: reserved before boot so the authorization snapshot exists. */ requestedSessionId?: string; /** ADR 0118: the session's apps, already resolved to public hostnames. The @@ -771,6 +775,7 @@ export interface TaskSessionsClient { } export interface CreateTaskDeps { + boundSessionCreator?: ReturnType; profiles: ProfileStore; images: ImagesClient; connectors: CustomConnectorSource; @@ -915,6 +920,9 @@ export function buildLaunchPolicy( integrationPolicyJson: sessionInput.integrationPolicyJson ?? "", integrationGrants: [...(sessionInput.integrationGrants ?? [])], integrationConnections: [...(sessionInput.integrationConnections ?? [])], + ...(sessionInput.mcpBindingEnvelope + ? { mcpBindingEnvelope: structuredClone(sessionInput.mcpBindingEnvelope) } + : {}), network: profile.network, secrets: [...profile.secrets], repos: [...profile.repos], @@ -1034,10 +1042,16 @@ export async function createSessionForExistingTask( capabilities: sessionInput.capabilities ?? [], integrationGrants: sessionInput.integrationGrants ?? [], integrationConnections: sessionInput.integrationConnections ?? [], + ...(sessionInput.mcpBindingEnvelope + ? { mcpBindingEnvelope: sessionInput.mcpBindingEnvelope } + : {}), integrationSnapshotHash: integrationSnapshotHash({ profileId: profile.id, integrationGrants: sessionInput.integrationGrants ?? [], integrationConnections: sessionInput.integrationConnections ?? [], + ...(sessionInput.mcpBindingEnvelope + ? { mcpBindingEnvelope: sessionInput.mcpBindingEnvelope } + : {}), }), ...(integrationPrincipalId ? { integrationPrincipalId } : {}), }); @@ -1047,7 +1061,10 @@ export async function createSessionForExistingTask( try { // When prompt is omitted (as it is for the finder), the session boots idle // so deterministic bootstrap can finish before SendPrompt wakes it. - const created = await deps.sessions.createSession(sessionInput); + const created = await createReservedTaskSession(deps, sessionInput, { + actorId: integrationPrincipalId ?? "", + inputId: sessionId, + }); createdSessionId = created.sessionId; if (created.sessionId !== sessionId) { throw new Error("coordinator returned a different reserved session ID"); @@ -1226,10 +1243,16 @@ export async function createTaskWithSession( capabilities: sessionInput.capabilities ?? [], integrationGrants: sessionInput.integrationGrants ?? [], integrationConnections: sessionInput.integrationConnections ?? [], + ...(sessionInput.mcpBindingEnvelope + ? { mcpBindingEnvelope: sessionInput.mcpBindingEnvelope } + : {}), integrationSnapshotHash: integrationSnapshotHash({ profileId: profile.id, integrationGrants: sessionInput.integrationGrants ?? [], integrationConnections: sessionInput.integrationConnections ?? [], + ...(sessionInput.mcpBindingEnvelope + ? { mcpBindingEnvelope: sessionInput.mcpBindingEnvelope } + : {}), }), integrationPrincipalId: params.ownerUserId, }); @@ -1269,7 +1292,10 @@ export async function createTaskWithSession( let createdSessionId: string | undefined; try { - const created = await deps.sessions.createSession(sessionInput); + const created = await createReservedTaskSession(deps, sessionInput, { + actorId: params.ownerUserId, + inputId: sessionId, + }); createdSessionId = created.sessionId; if (created.sessionId !== sessionId) { throw new Error("coordinator returned a different reserved session ID"); diff --git a/orchestrator/src/tools/__tests__/exec.test.ts b/orchestrator/src/tools/__tests__/exec.test.ts index dc47b60d3..0a7f2ae34 100644 --- a/orchestrator/src/tools/__tests__/exec.test.ts +++ b/orchestrator/src/tools/__tests__/exec.test.ts @@ -212,3 +212,22 @@ describe("tool exec plain functions", () => { expect(f.submitted).toEqual(["session-1:call-1"]); }); }); + +test("MCP resolution is session-local and absent runtime never calls a static handler", async () => { + const f = fixture(); + let resolvedSession: string | undefined; + const deps: ToolExecDeps = { + ...f.deps, + resolveMcpBoundTool: async ({ sessionId }) => { + resolvedSession = sessionId; + return undefined; + }, + }; + const outcome = await executeToolCall({ ...f.input, toolName: "mcp_docs_unknown" }, deps); + expect(resolvedSession).toBe(f.input.sessionId); + expect(outcome).toEqual({ + kind: "submit", + result: { error: "external MCP tool is unavailable" }, + }); + expect(f.handlerCalls).toEqual([]); +}); diff --git a/orchestrator/src/tools/coordination.ts b/orchestrator/src/tools/coordination.ts index 0c8450e1c..cbde8c1a1 100644 --- a/orchestrator/src/tools/coordination.ts +++ b/orchestrator/src/tools/coordination.ts @@ -1,5 +1,8 @@ /** Recursive session coordination tools (ADR 0113). */ +import { createReservedTaskSession } from "../mcp/session-create.ts"; +import { assertChildMcpCreationBlocked } from "../mcp/session-child.ts"; + import { createHash } from "node:crypto"; import { Code, ConnectError } from "@connectrpc/connect"; import { and, eq, ne, sql } from "drizzle-orm"; @@ -214,6 +217,7 @@ function snapshotConnectionStore(policy: TaskLaunchPolicy): IntegrationConnectio async function compileChildInput( parent: TaskRow, + spawningSessionId: string, args: z.output, sessionId: string, ): Promise { @@ -225,6 +229,7 @@ async function compileChildInput( // unattended sessions never carry human credentials extends to spawns. // The frozen policy already stamped org authority for such a parent (no // oauth_user mint sources), so no user seam below would fire anyway. + await assertChildMcpCreationBlocked(parent.id, spawningSessionId, policy.integrationGrants); const ownerId = parent.createdByUserId; const db = getDb(); const secrets = makeUserSecretStore(db); @@ -536,10 +541,14 @@ export function registerCoordinationTools(registry: ToolRegistry): void { throw new Error(`sub-session depth exceeds ${config.subSessionMaxDepth}`); } const rootTaskId = parent.rootTaskId ?? parent.id; - const input = await compileChildInput(parent, args, sessionId); + const input = await compileChildInput(parent, ctx.sessionId, args, sessionId); const policy = parent.launchPolicy!; + const { mcpBindingEnvelope: _parentEnvelope, ...basePolicy } = structuredClone(policy); const childPolicy: TaskLaunchPolicy = { - ...structuredClone(policy), + ...basePolicy, + integrationGrants: input.integrationGrants ?? [], + integrationConnections: input.integrationConnections ?? [], + ...(input.mcpBindingEnvelope ? { mcpBindingEnvelope: input.mcpBindingEnvelope } : {}), harness: input.harness ?? policy.harness, ...(input.modelRouter ? { modelRouter: input.modelRouter } : {}), ...(input.model ? { model: input.model } : {}), @@ -613,13 +622,19 @@ export function registerCoordinationTools(registry: ToolRegistry): void { role: "primary", profileId: policy.profileId, capabilities: [...policy.capabilities], - integrationGrants: structuredClone(policy.integrationGrants), - integrationConnections: structuredClone(policy.integrationConnections), + integrationGrants: structuredClone(input.integrationGrants ?? []), + integrationConnections: structuredClone(input.integrationConnections ?? []), + ...(input.mcpBindingEnvelope + ? { mcpBindingEnvelope: input.mcpBindingEnvelope } + : {}), integrationPrincipalId: parent.createdByUserId, integrationSnapshotHash: integrationSnapshotHash({ profileId: policy.profileId, - integrationGrants: policy.integrationGrants, - integrationConnections: policy.integrationConnections, + integrationGrants: input.integrationGrants ?? [], + integrationConnections: input.integrationConnections ?? [], + ...(input.mcpBindingEnvelope + ? { mcpBindingEnvelope: input.mcpBindingEnvelope } + : {}), }), }) .onConflictDoNothing(); @@ -651,7 +666,10 @@ export function registerCoordinationTools(registry: ToolRegistry): void { } }); try { - await sessions.createSession(input); + await createReservedTaskSession({ sessions }, input, { + actorId: parent.createdByUserId ?? "", + inputId: sessionId, + }); } catch (error) { try { await sessions.getSession({ sessionId }); diff --git a/orchestrator/src/tools/exec.ts b/orchestrator/src/tools/exec.ts index 760d738a8..53390cbf9 100644 --- a/orchestrator/src/tools/exec.ts +++ b/orchestrator/src/tools/exec.ts @@ -60,6 +60,7 @@ export type ToolExecutionOutcome = | { kind: "submit"; result: unknown }; export interface ToolExecDeps { + resolveMcpBoundTool?: import("../mcp/session-resolver.ts").SessionBindingResolver["resolveBoundTool"]; registry: ToolRegistry; resolveContext(sessionId: string): Promise; pendingCalls: PendingToolCallStore; @@ -113,6 +114,27 @@ export async function executeToolCall( "tool exec start", ); try { + if (input.toolName.startsWith("mcp_")) { + // Raw remote schemas never enter Zod or the global registry. Task 5d owns effects. + try { + const context = await deps.resolveContext(input.sessionId); + if (context.sessionId !== input.sessionId) throw new Error("session changed"); + const bound = await deps.resolveMcpBoundTool?.({ + sessionId: input.sessionId, + modelToolName: input.toolName, + }); + return { + kind: "submit", + result: { + error: bound + ? "external MCP execution is not configured" + : "external MCP tool is unavailable", + }, + }; + } catch { + return { kind: "submit", result: { error: "external MCP tool is unavailable" } }; + } + } const tool = deps.registry.get(input.toolName); if (!tool || tool.handling !== "handled") { throw new Error(`handled tool is not registered: ${input.toolName}`); diff --git a/orchestrator/src/tools/manifest.ts b/orchestrator/src/tools/manifest.ts index 976fe66d8..5c3617313 100644 --- a/orchestrator/src/tools/manifest.ts +++ b/orchestrator/src/tools/manifest.ts @@ -1,3 +1,5 @@ +import { compileMcpManifest, type McpSessionBindingSnapshot } from "../mcp/session-bindings.ts"; +import { MCP_LIMITS } from "../mcp/limits.ts"; import { z } from "zod"; import { @@ -35,3 +37,36 @@ export function compileToolManifest( nativeBindings: tool.nativeBindings ?? {}, })); } + +/** Preserve native descriptors and raw remote schemas, with one total wire-size bound. */ +export function appendMcpManifest( + nativeJson: string | undefined, + snapshot: McpSessionBindingSnapshot, +): ToolManifestEntry[] { + const native: unknown = nativeJson === undefined ? [] : JSON.parse(nativeJson); + if (!Array.isArray(native)) throw new Error("invalid native tool manifest"); + const entries: ToolManifestEntry[] = []; + for (const entry of native) { + if ( + !entry || + typeof entry !== "object" || + typeof entry.name !== "string" || + typeof entry.description !== "string" || + !entry.inputSchema || + typeof entry.inputSchema !== "object" || + !["sync", "deferred"].includes(entry.execution) || + !entry.nativeBindings || + typeof entry.nativeBindings !== "object" + ) + throw new Error("invalid native tool descriptor"); + entries.push(entry); + } + entries.push(...compileMcpManifest(snapshot)); + if ( + entries.length > MCP_LIMITS.toolsPerSession || + new Set(entries.map((entry) => entry.name)).size !== entries.length || + Buffer.byteLength(JSON.stringify(entries)) > MCP_LIMITS.manifestBytes + ) + throw new Error("session tool manifest exceeds limits or contains duplicate names"); + return structuredClone(entries); +} From f1bee94423bb3673df49d6109e01224c38b54fc8 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 02:25:27 -0500 Subject: [PATCH 019/279] Preserve created MCP session IDs for compensation --- .../src/__tests__/mcp-session-create.test.ts | 36 ++++++++ .../src/__tests__/task-create.test.ts | 86 +++++++++++++++++++ orchestrator/src/mcp/session-create.ts | 12 ++- orchestrator/src/rpc/task-create.ts | 8 +- 4 files changed, 139 insertions(+), 3 deletions(-) diff --git a/orchestrator/src/__tests__/mcp-session-create.test.ts b/orchestrator/src/__tests__/mcp-session-create.test.ts index 80df391a6..107dd2618 100644 --- a/orchestrator/src/__tests__/mcp-session-create.test.ts +++ b/orchestrator/src/__tests__/mcp-session-create.test.ts @@ -197,3 +197,39 @@ test("clearing child harness mode cannot detach the stored parent policy", async await creator.create(input, { actorId: "actor", inputId: "child-input" }); expect(receivedParent).toBe("actual-parent"); }); + +test("MCP response mismatch carries the actual created ID for compensation", async () => { + const authority = bindingAuthority(); + const input = compileBoundSessionCandidate( + { imageUri: "image", mode: "agent", harness: "claude", requestedSessionId: "reserved" }, + { + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }, + ); + const creator = makeBoundSessionCreator({ + control: { + createPolicySession: async () => + create(CreatePolicySessionResponseSchema, { session: { sessionId: "actual-created" } }), + }, + readReservation: async () => ({ + principalId: "actor", + parentSessionId: null, + envelope: input.mcpBindingEnvelope!, + connections: input.integrationConnections!, + }), + authorizeInput: async () => {}, + readAuthority: async () => authority, + readParentCurrent: async () => { + throw new Error("no parent"); + }, + enforceManagedDeny: async (expected) => expected, + }); + await expect( + creator.create(input, { actorId: "actor", inputId: "source" }), + ).rejects.toMatchObject({ + message: "MCP policy session response mismatch", + createdSessionId: "actual-created", + }); +}); diff --git a/orchestrator/src/__tests__/task-create.test.ts b/orchestrator/src/__tests__/task-create.test.ts index 4bae0f6d7..aff10e380 100644 --- a/orchestrator/src/__tests__/task-create.test.ts +++ b/orchestrator/src/__tests__/task-create.test.ts @@ -2155,3 +2155,89 @@ describe("connector settings (the administrator-chosen API host)", () => { expect(JSON.stringify(input)).toContain("api.getcortexapp.com"); }); }); + +/** Exercise the real closed MCP adapter at the injected session-client boundary. + * The production profile compiler's MCP activation guard remains unchanged. */ +async function mismatchedMcpSessions() { + const { create } = await import("@bufbuild/protobuf"); + const { CreatePolicySessionResponseSchema } = + await import("../gen/engram/mcp/v1/session_policy_pb.ts"); + const { compileBoundSessionCandidate, createReservedTaskSession, makeBoundSessionCreator } = + await import("../mcp/session-create.ts"); + const { bindingAuthority } = await import("./mcp-binding-fixture.ts"); + const authority = bindingAuthority(); + const sessions = fakeSessions(); + const createdIds: string[] = []; + sessions.createSession = async (request) => { + sessions.createReqs.push(request); + const candidate = compileBoundSessionCandidate(request, { + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }); + const creator = makeBoundSessionCreator({ + control: { + createPolicySession: async () => { + createdIds.push("actual-mcp-session"); + return create(CreatePolicySessionResponseSchema, { + session: { sessionId: "actual-mcp-session" }, + }); + }, + }, + readReservation: async () => ({ + principalId: "u", + parentSessionId: null, + envelope: candidate.mcpBindingEnvelope!, + connections: candidate.integrationConnections!, + }), + authorizeInput: async () => {}, + readAuthority: async () => authority, + readParentCurrent: async () => { + throw new Error("no parent"); + }, + enforceManagedDeny: async (expected) => expected, + }); + return createReservedTaskSession( + { + sessions: { + createSession: async () => { + throw new Error("MCP must not use generic creation"); + }, + }, + boundSessionCreator: creator, + }, + candidate, + { actorId: "u", inputId: request.requestedSessionId! }, + ); + }; + return { sessions, createdIds }; +} + +test("new task compensates the actual session ID from an MCP adapter mismatch", async () => { + const { sessions, createdIds } = await mismatchedMcpSessions(); + const deletes: string[] = []; + await expect( + createTaskWithSession(createDeps(sessions, recordingDb([], false, undefined, { deletes })), { + type: "chat", + ownerUserId: "u", + profileId: "p1", + }), + ).rejects.toThrow("MCP policy session response mismatch"); + expect(createdIds).toEqual(["actual-mcp-session"]); + expect(sessions.deletedIds).toEqual(["actual-mcp-session"]); + expect(deletes).toEqual(["task"]); +}); + +test("existing task compensates the actual session ID from an MCP adapter mismatch", async () => { + const { sessions, createdIds } = await mismatchedMcpSessions(); + const deletes: string[] = []; + await expect( + createSessionForExistingTask( + createDeps(sessions, recordingDb([], false, undefined, { deletes })), + { taskId: "task-existing", profileId: "p1", role: "finder", ownerUserId: "u" }, + ), + ).rejects.toThrow("MCP policy session response mismatch"); + expect(createdIds).toEqual(["actual-mcp-session"]); + expect(sessions.deletedIds).toEqual(["actual-mcp-session"]); + expect(deletes).toEqual(["task_session"]); +}); diff --git a/orchestrator/src/mcp/session-create.ts b/orchestrator/src/mcp/session-create.ts index 8d75ae4d8..88e98ecbf 100644 --- a/orchestrator/src/mcp/session-create.ts +++ b/orchestrator/src/mcp/session-create.ts @@ -78,6 +78,13 @@ export interface BoundSessionCreatorDeps { readParentCurrent(sessionId: string): Promise; enforceManagedDeny?(expected: ManagedDenyAck, policy: ManagedDenyPolicy): Promise; } +/** Retain the actual created session so callers can compensate a rejected response. */ +export class McpSessionCreationMismatchError extends Error { + constructor(readonly createdSessionId: string) { + super("MCP policy session response mismatch"); + this.name = "McpSessionCreationMismatchError"; + } +} /** Closed adapter for Task 6 composition. Generic create never initializes write authority. */ export function makeBoundSessionCreator(deps: BoundSessionCreatorDeps) { return { @@ -143,8 +150,9 @@ export function makeBoundSessionCreator(deps: BoundSessionCreatorDeps) { }, }, }); - if (response.session?.sessionId !== sessionId) - throw new Error("MCP policy session response mismatch"); + if (!response.session?.sessionId) throw new Error("MCP policy session response mismatch"); + if (response.session.sessionId !== sessionId) + throw new McpSessionCreationMismatchError(response.session.sessionId); return response.session; }, }; diff --git a/orchestrator/src/rpc/task-create.ts b/orchestrator/src/rpc/task-create.ts index e96c0f5d0..92a7f1486 100644 --- a/orchestrator/src/rpc/task-create.ts +++ b/orchestrator/src/rpc/task-create.ts @@ -17,7 +17,11 @@ * policy. */ -import { createReservedTaskSession, type makeBoundSessionCreator } from "../mcp/session-create.ts"; +import { + createReservedTaskSession, + McpSessionCreationMismatchError, + type makeBoundSessionCreator, +} from "../mcp/session-create.ts"; import { resolveIntegrationHost } from "../integrations/run-op.ts"; import { ConnectError, Code } from "@connectrpc/connect"; @@ -1075,6 +1079,7 @@ export async function createSessionForExistingTask( }); } } catch (err) { + if (err instanceof McpSessionCreationMismatchError) createdSessionId = err.createdSessionId; // Compensation must never mask the original failure: guard every step and // log what it could not undo. On an ID mismatch, the session that leaks is // the one the coordinator ACTUALLY created, so delete that one. @@ -1304,6 +1309,7 @@ export async function createTaskWithSession( await tx.insert(sessionListenerTable).values({ sessionId }); }); } catch (err) { + if (err instanceof McpSessionCreationMismatchError) createdSessionId = err.createdSessionId; // Compensation must never mask the original failure: guard every step and // log what it could not undo. On an ID mismatch, the session that leaks is // the one the coordinator ACTUALLY created, so delete that one. From f191b301adbd209160b1dc618f49df8caebd6670 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 03:01:22 -0500 Subject: [PATCH 020/279] Bind MCP reviews to immutable request authority --- .../proto/engram/app/v1/integration.proto | 137 +- .../drizzle/0106_mcp_request_review.sql | 2 + .../drizzle/0107_mcp_admission_generation.sql | 2 + orchestrator/drizzle/meta/0106_snapshot.json | 8451 ++++++++++++++++ orchestrator/drizzle/meta/0107_snapshot.json | 8473 +++++++++++++++++ orchestrator/drizzle/meta/_journal.json | 16 +- .../src/__tests__/mcp-review-fixture.ts | 207 + .../src/__tests__/mcp-review-rpc.test.ts | 432 + .../src/__tests__/mcp-review-store.test.ts | 524 + orchestrator/src/__tests__/mcp-rpc.test.ts | 2 + orchestrator/src/__tests__/mcp-store.test.ts | 195 +- orchestrator/src/db/schema.ts | 4 + .../src/gen/engram/app/v1/integration_pb.ts | 670 +- orchestrator/src/mcp/contracts.ts | 135 +- orchestrator/src/mcp/journal-review.ts | 328 + orchestrator/src/mcp/journal.ts | 512 +- orchestrator/src/mcp/review-state.ts | 279 + ...gration-IntegrationService_connectquery.ts | 5 + web/src/gen/engram/app/v1/integration_pb.ts | 670 +- 19 files changed, 20723 insertions(+), 321 deletions(-) create mode 100644 orchestrator/drizzle/0106_mcp_request_review.sql create mode 100644 orchestrator/drizzle/0107_mcp_admission_generation.sql create mode 100644 orchestrator/drizzle/meta/0106_snapshot.json create mode 100644 orchestrator/drizzle/meta/0107_snapshot.json create mode 100644 orchestrator/src/__tests__/mcp-review-fixture.ts create mode 100644 orchestrator/src/__tests__/mcp-review-rpc.test.ts create mode 100644 orchestrator/src/__tests__/mcp-review-store.test.ts create mode 100644 orchestrator/src/mcp/journal-review.ts create mode 100644 orchestrator/src/mcp/review-state.ts diff --git a/crates/engram-protocol/proto/engram/app/v1/integration.proto b/crates/engram-protocol/proto/engram/app/v1/integration.proto index fc687addc..a85347642 100644 --- a/crates/engram-protocol/proto/engram/app/v1/integration.proto +++ b/crates/engram-protocol/proto/engram/app/v1/integration.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package engram.app.v1; +import "engram/app/v1/session.proto"; + // ADR 0057 C3: orchestrator-native connector catalog CRUD (Plane B). Connectors // live in the orchestrator DB (the `connector` table, C1); the built-ins // (github/datadog) are read-only file seeds surfaced alongside custom ones. @@ -54,12 +56,13 @@ service IntegrationService { rpc GetGoogleCloudSetup(GetGoogleCloudSetupRequest) returns (GetGoogleCloudSetupResponse); rpc DiscoverMcp(DiscoverMcpRequest) returns (DiscoverMcpResponse); rpc PublishMcp(PublishMcpRequest) returns (PublishMcpResponse); - rpc PutMcpCredential(PutMcpCredentialRequest) returns (McpCredentialResponse); + rpc PutMcpCredential(PutMcpCredentialRequest) returns (PutMcpCredentialResponse); rpc GetMcpSetup(GetMcpSetupRequest) returns (GetMcpSetupResponse); - rpc CompleteMcpAuthorization(CompleteMcpAuthorizationRequest) returns (McpCredentialResponse); - rpc RecoverMcpCredential(RecoverMcpCredentialRequest) returns (McpCredentialResponse); + rpc CompleteMcpAuthorization(CompleteMcpAuthorizationRequest) returns (CompleteMcpAuthorizationResponse); + rpc RecoverMcpCredential(RecoverMcpCredentialRequest) returns (RecoverMcpCredentialResponse); rpc AuthorizeMcp(AuthorizeMcpRequest) returns (AuthorizeMcpResponse); rpc DisconnectMcp(DisconnectMcpRequest) returns (DisconnectMcpResponse); + rpc GetSessionMcpState(GetSessionMcpStateRequest) returns (GetSessionMcpStateResponse); rpc ListMcpApprovals(ListMcpApprovalsRequest) returns (ListMcpApprovalsResponse); rpc DecideMcpApproval(DecideMcpApprovalRequest) returns (DecideMcpApprovalResponse); rpc CancelMcpCall(CancelMcpCallRequest) returns (CancelMcpCallResponse); @@ -416,19 +419,27 @@ message McpApproval { string request_digest = 7; string argument_hash = 8; string expires_at = 9; + string original_arguments = 10; + McpTool reviewed_tool = 11; + repeated string argument_constraint_documents_json = 12; + McpApprovalReviewContext expected_review_context = 13; } message ListMcpApprovalsRequest { string session_id = 1; + string after_execution_id = 2; } message ListMcpApprovalsResponse { repeated McpApproval approvals = 1; + string next_execution_id = 2; } message DecideMcpApprovalRequest { string execution_id = 1; bool approve = 2; + McpApprovalReviewContext expected_review_context = 3; } message DecideMcpApprovalResponse { string state = 1; + McpApprovalReceipt receipt = 2; } message CancelMcpCallRequest { string execution_id = 1; @@ -468,7 +479,19 @@ message McpCredentialStatus { int64 version = 5; int64 reservation_generation = 6; } -message McpCredentialResponse { +message PutMcpCredentialResponse { + IntegrationConnection connection = 1; + McpCredentialStatus credential = 2; +} +message CompleteMcpAuthorizationResponse { + IntegrationConnection connection = 1; + McpCredentialStatus credential = 2; +} +message RecoverMcpCredentialResponse { + IntegrationConnection connection = 1; + McpCredentialStatus credential = 2; +} +message McpCredentialState { IntegrationConnection connection = 1; McpCredentialStatus credential = 2; } @@ -492,7 +515,7 @@ message GetMcpSetupResponse { bool enabled = 1; repeated McpClientRegistration registrations = 2; repeated McpApprovedDestination destinations = 3; - McpCredentialResponse current = 4; + McpCredentialState current = 4; bool transition_pending = 5; string flow_id = 6; string flow_expires_at = 7; @@ -513,3 +536,107 @@ message RecoverMcpCredentialRequest { uint32 expected_revision = 2; uint32 expected_generation = 3; } + +// Exact server review. Clients must return it unchanged when they decide. +message McpApprovalReviewContext { + string execution_id = 1; + string session_id = 2; + string run_id = 3; + string tool_call_id = 4; + string request_digest = 5; + string argument_hash = 6; + string binding_hash = 7; + string connection_id = 8; + string catalog_id = 9; + uint32 catalog_revision = 10; + uint32 config_revision = 11; + uint32 auth_generation = 12; + string credential_identity = 13; + string upstream_name = 14; + string tool_fingerprint = 15; + string context_digest = 16; + uint32 journal_revision = 17; + string expires_at = 18; + string policy_revision = 19; + string binding_epoch = 20; +} +message McpApprovalReceipt { + string execution_id = 1; + string decision = 2; + string actor_id = 3; + string decided_at = 4; + uint32 journal_revision = 5; + McpApprovalReviewContext reviewed_context = 6; +} +message McpExecutionReview { + string execution_id = 1; + string connection_id = 2; + string state = 3; + uint32 journal_revision = 4; + bool result_unavailable = 5; + McpApproval review = 6; + repeated string allowed_actions = 7; + string reason = 8; + string tool_call_id = 9; + string run_id = 10; + string request_digest = 11; + string upstream_name = 12; + string approval_decision = 13; + string original_arguments = 14; + McpTool reviewed_tool = 15; + repeated string argument_constraint_documents_json = 16; + string binding_hash = 17; +} +message McpResolutionReceipt { + string execution_id = 1; + string actor_id = 2; + string outcome = 3; + string created_at = 4; +} +message McpBindingReceipt { + string binding_hash = 1; + string binding_epoch = 2; + bool current = 3; + repeated string stale_reasons = 4; + repeated string allowed_actions = 5; +} +message GetSessionMcpStateRequest { + string session_id = 1; + string after_execution_id = 2; + uint32 page_size = 3; + optional string plan_tool_call_id = 4; + string after_binding_tool_name = 5; + uint32 binding_page_size = 6; + bool omit_bindings = 7; + bool omit_executions = 8; +} +message GetSessionMcpStateResponse { + McpBindingReceipt binding_receipt = 1; + repeated McpExecutionReview executions = 2; + repeated McpApproval approvals = 3; + repeated string unknown_write_blocks = 4; + repeated McpResolutionReceipt resolution_receipts = 5; + repeated string allowed_actions = 6; + string availability = 7; + string next_execution_id = 8; + GetMcpWritePolicyResponse write_policy = 9; + repeated McpBoundToolSummary binding_tools = 10; + string next_binding_tool_name = 11; + bool has_more_binding_tools = 12; + bool has_more_executions = 13; +} + +// Immutable session grant. Schemas remain independent documents with their own roots. +message McpBoundToolSummary { + string connection_id = 1; + string connection_alias = 2; + string model_tool_name = 3; + McpTool reviewed_tool = 4; + repeated string argument_constraint_documents_json = 5; + bool automation_write = 6; + bool unrestricted = 7; + string catalog_id = 8; + uint32 catalog_revision = 9; + uint32 config_revision = 10; + uint32 auth_generation = 11; +} diff --git a/orchestrator/drizzle/0106_mcp_request_review.sql b/orchestrator/drizzle/0106_mcp_request_review.sql new file mode 100644 index 000000000..ea4737f84 --- /dev/null +++ b/orchestrator/drizzle/0106_mcp_request_review.sql @@ -0,0 +1,2 @@ +ALTER TABLE "mcp_execution" ADD COLUMN "original_arguments" text;--> statement-breakpoint +ALTER TABLE "mcp_execution" ADD COLUMN "request_context" jsonb; \ No newline at end of file diff --git a/orchestrator/drizzle/0107_mcp_admission_generation.sql b/orchestrator/drizzle/0107_mcp_admission_generation.sql new file mode 100644 index 000000000..1b29fa51d --- /dev/null +++ b/orchestrator/drizzle/0107_mcp_admission_generation.sql @@ -0,0 +1,2 @@ +ALTER TABLE "mcp_admission" ADD COLUMN "reservation_id" text DEFAULT gen_random_uuid()::text NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "mcp_admission_reservation_idx" ON "mcp_admission" USING btree ("reservation_id"); \ No newline at end of file diff --git a/orchestrator/drizzle/meta/0106_snapshot.json b/orchestrator/drizzle/meta/0106_snapshot.json new file mode 100644 index 000000000..2ce1051e3 --- /dev/null +++ b/orchestrator/drizzle/meta/0106_snapshot.json @@ -0,0 +1,8451 @@ +{ + "id": "f25a3416-20cb-4d4a-897f-6c135d22a0f3", + "prevId": "85dcfc01-8ed5-4cba-bd2d-709747d6a76a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_credential": { + "name": "mcp_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_arguments": { + "name": "original_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_binding_envelope": { + "name": "mcp_binding_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/0107_snapshot.json b/orchestrator/drizzle/meta/0107_snapshot.json new file mode 100644 index 000000000..9988a72eb --- /dev/null +++ b/orchestrator/drizzle/meta/0107_snapshot.json @@ -0,0 +1,8473 @@ +{ + "id": "66978c05-90c9-4f3c-bf04-3bfb2f7cc82c", + "prevId": "f25a3416-20cb-4d4a-897f-6c135d22a0f3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_credential": { + "name": "mcp_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()::text" + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_reservation_idx": { + "name": "mcp_admission_reservation_idx", + "columns": [ + { + "expression": "reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_arguments": { + "name": "original_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_binding_envelope": { + "name": "mcp_binding_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/_journal.json b/orchestrator/drizzle/meta/_journal.json index a19cbb1eb..b5498a0e4 100644 --- a/orchestrator/drizzle/meta/_journal.json +++ b/orchestrator/drizzle/meta/_journal.json @@ -743,6 +743,20 @@ "when": 1793602718404, "tag": "0105_mcp_binding_envelope", "breakpoints": true + }, + { + "idx": 106, + "version": "7", + "when": 1793604760959, + "tag": "0106_mcp_request_review", + "breakpoints": true + }, + { + "idx": 107, + "version": "7", + "when": 1793605822727, + "tag": "0107_mcp_admission_generation", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/orchestrator/src/__tests__/mcp-review-fixture.ts b/orchestrator/src/__tests__/mcp-review-fixture.ts new file mode 100644 index 000000000..841105124 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-review-fixture.ts @@ -0,0 +1,207 @@ +import { create } from "@bufbuild/protobuf"; +import { eq } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { user, task, taskSession, profile, mcpCatalog, mcpAdmissionScope } from "../db/schema.ts"; +import { makeIntegrationConnectionStore } from "../db/integration-connections.ts"; +import { makeMcpRepository } from "../mcp/repository.ts"; +import { makeMcpJournal } from "../mcp/journal.ts"; +import { compileSessionBindings } from "../mcp/session-bindings.ts"; +import { bindingSnapshotColumns } from "../mcp/binding-store.ts"; +import { canonicalDigest } from "../mcp/hash.ts"; +import { McpWritePolicyRefSchema } from "../gen/engram/app/v1/session_pb.ts"; + +/** The stored binding and policy are test fixtures. No host ACK or VM enforcement is claimed. */ +export async function reviewFixture(documents: Record[] = [], toolCount = 1) { + const profileId = crypto.randomUUID(); + const db = getDb(), + actorId = crypto.randomUUID(), + taskId = crypto.randomUUID(), + sessionId = crypto.randomUUID(), + deploymentId = crypto.randomUUID(); + const now = new Date("2026-01-01T00:00:00Z"); + const connections = makeIntegrationConnectionStore(db), + repository = makeMcpRepository(db); + await db.insert(user).values({ + id: actorId, + name: "Review owner", + email: `${actorId}@example.test`, + role: "user", + createdAt: now, + updatedAt: now, + }); + const config = { + endpoint: "https://tools.example.test/mcp", + zone: "public", + auth: { kind: "api_key" as const, secretRef: "mcp.test.key" }, + }; + const connection = await connections.create({ + alias: `test-${crypto.randomUUID()}`, + provider: "mcp", + displayName: "Test tools", + config, + }); + await repository.setAuthorization(connection.id, 1, 1, "test-account", now); + const current = (await connections.get(connection.id))!; + const tool = { + upstreamName: "Write.Record", + description: "Write a record", + inputSchema: { type: "object" }, + }; + const tools = Array.from({ length: toolCount }, (_, i) => ({ + ...tool, + upstreamName: i === 0 ? tool.upstreamName : `Write.Record${i}`, + })); + const draft = await repository.saveDraft(connection.id, current.mcp!, tools, actorId, now); + const catalog = await repository.publish( + connection.id, + draft.id, + 0, + tools.map((tool) => ({ ...tool, classification: "write", enabled: true })), + actorId, + now, + ); + await repository.setEnabled(connection.id, 1, true, now); + const grant = { + connectionId: connection.id, + operation: "invoke", + resourceConstraints: [], + credentialScope: "org" as const, + mcp: { + upstreamName: tool.upstreamName, + automationWrite: true, + unrestricted: documents.length === 0, + argumentConstraintDocuments: documents, + }, + }; + const grants = tools.map((tool) => ({ + ...grant, + mcp: { ...grant.mcp, upstreamName: tool.upstreamName }, + })); + const authority = { + profileId, + profileGrants: grants, + connections: [ + { + id: connection.id, + catalogId: catalog.id, + alias: connection.alias, + enabled: true, + config, + state: { ...current.mcp!, catalogRevision: 1 }, + tools: [catalog.tools[0]!], + }, + ], + operatorPolicies: [ + { + zoneId: "public", + policyRevision: "1", + policyDigest: canonicalDigest({ policy: 1 }), + authorities: ["tools.example.test"], + aliases: [], + }, + ], + }; + // The repository published this catalog as reviewed, with explicit write classification. + const { reviewCatalog } = await import("../mcp/catalog.ts"); + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: authority.profileId, integrationGrants: grants }, + liveAuthority: { + ...authority, + connections: [{ ...authority.connections[0]!, tools: reviewCatalog(catalog.tools) }], + }, + bindingEpoch: 0, + }); + await db.insert(profile).values({ + id: profileId, + name: "Review profile", + imageId: "image", + harness: "claude", + integrationGrants: grants, + }); + await db.insert(task).values({ id: taskId, type: "chat", createdByUserId: actorId }); + await db.insert(taskSession).values({ + taskId, + sessionId, + ...bindingSnapshotColumns(snapshot, [ + { + id: connection.id, + alias: connection.alias, + provider: "mcp", + displayName: connection.displayName, + config: connection.config, + }, + ]), + }); + const binding = snapshot.immutableBindings[0]!, + bound = binding.tools[0]!; + const resolvedTool = { + bindingHash: snapshot.bindingHash, + binding, + modelToolName: bound.name, + reviewedTool: bound.tool, + effectiveGrant: bound.grant, + ancestorAuthorityRefs: snapshot.ancestorAuthorityRefs, + execution: "deferred" as const, + }; + const requestPolicyContext = { + sessionId, + runId: "run-1", + toolCallId: "call-1", + acceptedPolicyRef: create(McpWritePolicyRefSchema, { + sessionId, + policyRevision: 1n, + bindingEpoch: 0n, + }), + acceptedWriteEligibility: "write_eligible", + ancestorPolicyRefs: [], + }; + const input = { + taskId, + sessionId, + runId: "run-1", + toolCallId: "call-1", + principalId: actorId, + connectionId: connection.id, + catalogId: catalog.id, + bindingHash: snapshot.bindingHash, + argumentHash: canonicalDigest({ x: 1 }), + originalArguments: '{ "x": 1 }', + resolvedTool, + requestPolicyContext, + upstreamName: tool.upstreamName, + configRevision: 1, + authGeneration: current.mcp!.authGeneration, + classification: "write" as const, + deadline: new Date(now.getTime() + 600_000), + }; + return { + db, + authority: { + ...authority, + connections: [{ ...authority.connections[0]!, tools: reviewCatalog(catalog.tools) }], + }, + profileId, + actorId, + draft, + actor: actorId, + taskId, + sessionId, + deploymentId, + now, + connections, + repository, + connection, + catalog, + snapshot, + input, + journal: makeMcpJournal(db), + async cleanup() { + await db.delete(task).where(eq(task.id, taskId)); + await db.delete(mcpAdmissionScope).where(eq(mcpAdmissionScope.id, deploymentId)); + await db.delete(mcpCatalog).where(eq(mcpCatalog.connectionId, connection.id)); + await connections.delete(connection.id); + await db.delete(profile).where(eq(profile.id, profileId)); + await db.delete(user).where(eq(user.id, actorId)); + }, + }; +} diff --git a/orchestrator/src/__tests__/mcp-review-rpc.test.ts b/orchestrator/src/__tests__/mcp-review-rpc.test.ts new file mode 100644 index 000000000..b5802b35c --- /dev/null +++ b/orchestrator/src/__tests__/mcp-review-rpc.test.ts @@ -0,0 +1,432 @@ +import { expect, test } from "bun:test"; +import { createClient, createRouterTransport, Code, ConnectError } from "@connectrpc/connect"; +import { createConnectTransport } from "@connectrpc/connect-node"; +import { create, toBinary, toJson } from "@bufbuild/protobuf"; +import type { AddressInfo } from "node:net"; +import { Hono } from "hono"; +import { eq } from "drizzle-orm"; +import { checkDb } from "../db/client.ts"; +import { task, user, pendingToolCall, mcpApproval } from "../db/schema.ts"; +import { reviewFixture } from "./mcp-review-fixture.ts"; +import { buildServer } from "../server.ts"; +import { + IntegrationService, + GetSessionMcpStateResponseSchema, +} from "../gen/engram/app/v1/integration_pb.ts"; +import { SessionService } from "../gen/engram/app/v1/session_pb.ts"; +import { mcpHandlers, type McpServices } from "../mcp/contracts.ts"; +import { makePendingToolCallStore } from "../tools/pending-tool-calls.ts"; +import { makeExternalToolCompletionGuard } from "../rpc/tool-completion-guard.ts"; +import { registerPassthrough } from "../rpc/passthrough.ts"; + +const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; +async function wire(f: Awaited>, producers = true) { + const service: McpServices = { enabled: true, repository: f.repository, journal: f.journal }; + // Injected policy and resolver are fixtures, not a live coordinator/host ACK. + if (producers) + service.reviewProducers = { + bindings: { readCurrent: async () => structuredClone(f.snapshot) }, + policy: { + readWritePolicy: async () => ({ + state: { + policy: { + ref: f.input.requestPolicyContext.acceptedPolicyRef, + writeEligibility: "write_eligible", + current: true, + initialized: true, + }, + planReview: { + policyRef: f.input.requestPolicyContext.acceptedPolicyRef, + toolCallId: "plan-a", + runId: "run-1", + eventIdx: 1n, + requestDigest: "plan-digest", + argsJson: '{"plan":"original"}', + }, + }, + }), + }, + }; + let forwarded = 0; + const upstream = createRouterTransport((router) => + router.service(SessionService, { + completeToolCall: async (req) => { + forwarded++; + return { sessionId: req.sessionId }; + }, + }), + ); + const server = buildServer(new Hono(), (router) => { + const getSession = async () => ({ + user: { id: f.actorId, role: "admin", email: `${f.actorId}@example.test` }, + }); + router.service( + IntegrationService, + mcpHandlers({ + services: service, + connections: f.connections, + getSession, + now: () => f.now, + connectionToProto: (row) => ({ id: row.id }), + }), + ); + registerPassthrough( + router, + [{ service: SessionService, methods: ["CompleteToolCall"] }], + upstream, + getSession, + async () => f.actorId, + { + "SessionService.CompleteToolCall": makeExternalToolCompletionGuard({ + pendingCalls: makePendingToolCallStore(f.db), + }), + }, + ); + }); + const url = await new Promise((resolve) => + server.listen(0, "127.0.0.1", () => + resolve(`http://127.0.0.1:${(server.address() as AddressInfo).port}/rpc`), + ), + ); + const transport = createConnectTransport({ baseUrl: url, httpVersion: "1.1" }); + return { + client: createClient(IntegrationService, transport), + session: createClient(SessionService, transport), + service, + forwarded: () => forwarded, + close: () => + new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ), + }; +} +async function code(promise: Promise, expected: Code) { + try { + await promise; + throw new Error("expected denial"); + } catch (error) { + expect(error).toBeInstanceOf(ConnectError); + expect((error as ConnectError).code).toBe(expected); + } +} +test.skipIf(!reachable)( + "registered review handlers return original arguments and bind decisions to all context", + async () => { + const f = await reviewFixture(), + s = await wire(f); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const state = await s.client.getSessionMcpState({ + sessionId: f.sessionId, + planToolCallId: "plan-a", + }); + expect(state.availability).toBe("available"); + expect(state.writePolicy?.planReview?.argsJson).toBe('{"plan":"original"}'); + expect(state.approvals[0]!.originalArguments).toBe(f.input.originalArguments); + expect(state.executions[0]!.allowedActions).toContain("approve"); + expect(JSON.stringify(toJson(GetSessionMcpStateResponseSchema, state))).not.toContain( + "mcp.test.key", + ); + const expected = state.approvals[0]!.expectedReviewContext!; + await code( + s.client.decideMcpApproval({ executionId: call.id, approve: true }), + Code.InvalidArgument, + ); + await code( + s.client.decideMcpApproval({ + executionId: call.id, + approve: true, + expectedReviewContext: { ...expected, argumentHash: "changed" }, + }), + Code.Aborted, + ); + const result = await s.client.decideMcpApproval({ + executionId: call.id, + approve: true, + expectedReviewContext: expected, + }); + expect(result.state).toBe("authorized"); + expect(result.receipt?.reviewedContext).toEqual(expected); + expect(result.receipt?.actorId).toBe(f.actorId); + expect((await f.journal.get(call.id))!.bootId).toBeNull(); + } finally { + await s.close(); + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "fresh authorization denies cached admin on empty reads and changed permission decisions", + async () => { + const f = await reviewFixture(), + s = await wire(f); + try { + await f.db.update(task).set({ createdByUserId: "other-owner" }).where(eq(task.id, f.taskId)); + await code(s.client.getSessionMcpState({ sessionId: f.sessionId }), Code.PermissionDenied); + await code(s.client.listMcpApprovals({ sessionId: f.sessionId }), Code.PermissionDenied); + await f.db.update(task).set({ createdByUserId: f.actorId }).where(eq(task.id, f.taskId)); + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const read = await s.client.listMcpApprovals({ sessionId: f.sessionId }); + await f.db.update(user).set({ banned: true }).where(eq(user.id, f.actorId)); + await code( + s.client.decideMcpApproval({ + executionId: call.id, + approve: true, + expectedReviewContext: read.approvals[0]!.expectedReviewContext, + }), + Code.PermissionDenied, + ); + expect((await f.journal.get(call.id))!.state).toBe("waiting_approval"); + } finally { + await s.close(); + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "generic completion and forged guest projection cannot change MCP authority", + async () => { + const f = await reviewFixture(), + s = await wire(f); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const projection = makePendingToolCallStore(f.db); + await projection.recordRequested({ + sessionId: f.sessionId, + toolCallId: f.input.toolCallId, + toolName: f.input.resolvedTool.modelToolName, + handling: "handled", + requestedAt: f.now, + }); + await code( + s.session.completeToolCall({ + sessionId: f.sessionId, + toolCallId: f.input.toolCallId, + resultJson: '{"decision":"approve","ok":true}', + }), + Code.PermissionDenied, + ); + await projection.markSubmitted(f.sessionId, f.input.toolCallId, f.now); + await projection.markCompleted(f.sessionId, f.input.toolCallId, f.now); + expect((await f.journal.get(call.id))!.state).toBe("waiting_approval"); + const [approval] = await f.db + .select() + .from(mcpApproval) + .where(eq(mcpApproval.executionId, call.id)); + expect(approval!.decision).toBe("pending"); + expect(approval!.consumedAt).toBeNull(); + expect(s.forwarded()).toBe(0); + } finally { + await f.db.delete(pendingToolCall).where(eq(pendingToolCall.sessionId, f.sessionId)); + await s.close(); + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "state pages are bounded and missing producers never offer approval", + async () => { + const f = await reviewFixture(), + s = await wire(f, false); + try { + for (let i = 0; i < 18; i++) + await f.journal.create( + { + ...f.input, + toolCallId: `page-${i}`, + requestPolicyContext: { ...f.input.requestPolicyContext, toolCallId: `page-${i}` }, + }, + { kind: "approval" }, + f.now, + ); + const first = await s.client.getSessionMcpState({ sessionId: f.sessionId }); + expect(first.executions).toHaveLength(16); + expect(first.availability).toBe("unavailable"); + expect(first.approvals).toHaveLength(0); + expect(first.executions.flatMap((c) => c.allowedActions)).not.toContain("approve"); + const next = await s.client.getSessionMcpState({ + sessionId: f.sessionId, + afterExecutionId: first.nextExecutionId, + }); + expect(next.executions).toHaveLength(2); + expect( + new Set([...first.executions, ...next.executions].map((c) => c.executionId)).size, + ).toBe(18); + expect( + toBinary(GetSessionMcpStateResponseSchema, create(GetSessionMcpStateResponseSchema, first)) + .byteLength, + ).toBeLessThanOrEqual(2 * 1024 * 1024); + } finally { + await s.close(); + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "byte-limited pages preserve whole arguments, independent schema roots and int64 policy identity", + async () => { + const documents = [ + { + $schema: "http://json-schema.org/draft-07/schema#", + $id: "urn:test:constraint", + definitions: { value: { type: "string" } }, + type: "object", + properties: { value: { $ref: "#/definitions/value" } }, + }, + { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: "urn:test:constraint", + $defs: { value: { maxLength: 250000 } }, + type: "object", + properties: { value: { $ref: "#/$defs/value" } }, + }, + ]; + const f = await reviewFixture(documents), + s = await wire(f); + try { + f.input.requestPolicyContext.acceptedPolicyRef.policyRevision = 9007199254740993n; + const { canonicalDigest } = await import("../mcp/hash.ts"); + const args = { value: '"'.repeat(120000) }, + originalArguments = JSON.stringify(args); + for (let i = 0; i < 10; i++) { + const call = await f.journal.create( + { + ...f.input, + originalArguments, + argumentHash: canonicalDigest(args), + toolCallId: `large-${i}`, + requestPolicyContext: { ...f.input.requestPolicyContext, toolCallId: `large-${i}` }, + }, + { kind: "approval" }, + f.now, + ); + await f.journal.requestApproval(call.id, f.deploymentId, f.now); + } + const first = await s.client.getSessionMcpState({ sessionId: f.sessionId }); + expect(first.executions.length).toBeLessThan(10); + expect(first.executions.length).toBeGreaterThan(0); + const pages = [first]; + while (pages.at(-1)!.hasMoreExecutions) { + pages.push( + await s.client.getSessionMcpState({ + sessionId: f.sessionId, + afterExecutionId: pages.at(-1)!.nextExecutionId, + omitBindings: true, + }), + ); + if (pages.length > 10) throw new Error("state cursor did not advance"); + } + const approvals = pages.flatMap((page) => page.approvals); + expect(approvals).toHaveLength(10); + for (const item of approvals) { + expect(item.originalArguments).toBe(originalArguments); + expect(item.argumentConstraintDocumentsJson.map((value) => JSON.parse(value))).toEqual( + documents, + ); + expect(item.expectedReviewContext!.policyRevision).toBe("9007199254740993"); + } + for (const page of pages) { + expect(toBinary(GetSessionMcpStateResponseSchema, page).byteLength).toBeLessThanOrEqual( + 2 * 1024 * 1024, + ); + expect( + Buffer.byteLength(JSON.stringify(toJson(GetSessionMcpStateResponseSchema, page))), + ).toBeLessThanOrEqual(2 * 1024 * 1024); + } + } finally { + await s.close(); + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "no-call sessions expose complete immutable binding tools with independent paging", + async () => { + const documents = [ + { $schema: "http://json-schema.org/draft-07/schema#", $id: "urn:shared", type: "object" }, + { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: "urn:shared", + type: "object", + }, + ]; + const f = await reviewFixture(documents, 18), + s = await wire(f, false); + try { + const first = await s.client.getSessionMcpState({ sessionId: f.sessionId }); + expect(first.executions).toHaveLength(0); + expect(first.bindingTools).toHaveLength(16); + expect(first.bindingReceipt?.current).toBe(false); + expect(first.hasMoreBindingTools).toBe(true); + const second = await s.client.getSessionMcpState({ + sessionId: f.sessionId, + afterBindingToolName: first.nextBindingToolName, + omitExecutions: true, + }); + expect(second.bindingTools).toHaveLength(2); + expect(second.hasMoreBindingTools).toBe(false); + const tools = [...first.bindingTools, ...second.bindingTools]; + expect(new Set(tools.map((tool) => tool.modelToolName)).size).toBe(18); + for (const tool of tools) { + expect(tool.argumentConstraintDocumentsJson.map((value) => JSON.parse(value))).toEqual( + documents, + ); + expect(tool.catalogId).toBe(f.catalog.id); + expect(tool.unrestricted).toBe(false); + } + expect(JSON.stringify(toJson(GetSessionMcpStateResponseSchema, first))).not.toContain( + "mcp.test.key", + ); + expect(toBinary(GetSessionMcpStateResponseSchema, first).byteLength).toBeLessThanOrEqual( + 2 * 1024 * 1024, + ); + } finally { + await s.close(); + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)("a fresh policy lineage change invalidates a saved review", async () => { + const f = await reviewFixture(), + s = await wire(f); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const state = await s.client.getSessionMcpState({ sessionId: f.sessionId }); + const expected = state.approvals[0]!.expectedReviewContext!; + s.service.reviewProducers!.policy.readWritePolicy = async () => ({ + state: { + policy: { + ref: f.input.requestPolicyContext.acceptedPolicyRef, + writeEligibility: "write_eligible", + current: true, + initialized: true, + ancestorPolicyRefs: [ + { sessionId: "ancestor-changed", bindingEpoch: 0n, policyRevision: 2n }, + ], + }, + }, + }); + await code( + s.client.decideMcpApproval({ + executionId: call.id, + approve: true, + expectedReviewContext: expected, + }), + Code.Aborted, + ); + const current = await s.client.getSessionMcpState({ sessionId: f.sessionId }); + expect(current.approvals).toHaveLength(0); + expect(current.executions[0]!.allowedActions).not.toContain("approve"); + expect((await f.journal.get(call.id))!.state).toBe("waiting_approval"); + } finally { + await s.close(); + await f.cleanup(); + } +}); diff --git a/orchestrator/src/__tests__/mcp-review-store.test.ts b/orchestrator/src/__tests__/mcp-review-store.test.ts new file mode 100644 index 000000000..58ccf762f --- /dev/null +++ b/orchestrator/src/__tests__/mcp-review-store.test.ts @@ -0,0 +1,524 @@ +import { expect, test } from "bun:test"; +import { eq } from "drizzle-orm"; +import { checkDb, getDb } from "../db/client.ts"; +import { task, taskSession, user } from "../db/schema.ts"; +import { makeMcpJournal } from "../mcp/journal.ts"; + +const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; +test.skipIf(!reachable)( + "empty approval reads still require a current human owner or admin", + async () => { + const db = getDb(); + const actorId = crypto.randomUUID(), + taskId = crypto.randomUUID(), + sessionId = crypto.randomUUID(); + try { + await db.insert(user).values({ + id: actorId, + name: "Review reader", + email: `${actorId}@example.test`, + role: "user", + createdAt: new Date(), + updatedAt: new Date(), + }); + await db.insert(task).values({ id: taskId, type: "chat", createdByUserId: "another-owner" }); + await db.insert(taskSession).values({ taskId, sessionId }); + const journal = makeMcpJournal(db); + // A cached administrator role must not grant access, even when no calls exist. + await expect(journal.pending(sessionId, actorId, new Date())).rejects.toThrow( + "owner or administrator", + ); + } finally { + await db.delete(task).where(eq(task.id, taskId)); + await db.delete(user).where(eq(user.id, actorId)); + } + }, +); + +import { reviewFixture } from "./mcp-review-fixture.ts"; +test.skipIf(!reachable)( + "journal freezes original bytes and rejects argument hash mismatch", + async () => { + const f = await reviewFixture(); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + expect(call.originalArguments).toBe(f.input.originalArguments); + await expect( + f.journal.create( + { ...f.input, toolCallId: "changed", originalArguments: '{"x":2}' }, + { kind: "approval" }, + f.now, + ), + ).rejects.toThrow("argument"); + } finally { + await f.cleanup(); + } + }, +); + +import { sql } from "drizzle-orm"; +import { mcpApproval, mcpAdmission, mcpExecution, profile } from "../db/schema.ts"; +import { canonicalDigest } from "../mcp/hash.ts"; +import { reviewContext, requestPolicyContext } from "../mcp/journal-review.ts"; + +async function pending(f: Awaited>) { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, f.now); + return { call, approval, expected: reviewContext(call, approval) }; +} +test.skipIf(!reachable)( + "same request races keep the first bytes and immutable complete context", + async () => { + const f = await reviewFixture(); + try { + const calls = await Promise.all( + Array.from({ length: 4 }, () => f.journal.create(f.input, { kind: "approval" }, f.now)), + ); + expect(new Set(calls.map((c) => c.id)).size).toBe(1); + const original = calls[0]!; + expect(requestPolicyContext(original.requestContext!).acceptedPolicyRef.policyRevision).toBe( + 1n, + ); + const changed = { + ...f.input, + originalArguments: '{"x":2}', + argumentHash: canonicalDigest({ x: 2 }), + }; + await expect(f.journal.create(changed, { kind: "approval" }, f.now)).rejects.toThrow( + "digest", + ); + await expect( + f.journal.create({ ...f.input, originalArguments: '{"x":1}' }, { kind: "approval" }, f.now), + ).rejects.toThrow("digest"); + await expect( + Promise.resolve( + f.db + .update(mcpExecution) + .set({ originalArguments: '{"x":2}', state: "canceled", version: 2 }) + .where(eq(mcpExecution.id, original.id)), + ), + ).rejects.toThrow(); + expect((await f.journal.get(original.id))!.originalArguments).toBe(f.input.originalArguments); + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "every expected review field is exact and concurrent decisions have one winner", + async () => { + const f = await reviewFixture(); + try { + const { call, expected } = await pending(f); + for (const key of Object.keys(expected) as (keyof typeof expected)[]) { + const value = expected[key]; + await expect( + f.journal.decide( + call.id, + f.actorId, + true, + { ...expected, [key]: typeof value === "number" ? value + 1 : `${value}-changed` }, + f.now, + ), + ).rejects.toThrow("stale"); + } + const results = await Promise.allSettled( + [true, false].map((approve) => + f.journal.decide(call.id, f.actorId, approve, expected, f.now), + ), + ); + expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(1); + const [approval] = await f.db + .select() + .from(mcpApproval) + .where(eq(mcpApproval.executionId, call.id)); + expect(approval!.actorId).toBe(f.actorId); + expect(approval!.consumedAt).toBeNull(); + } finally { + await f.cleanup(); + } + }, +); +async function waitForBlocked(db: ReturnType, blocker: number) { + const deadline = Date.now() + 1000; + while (Date.now() < deadline) { + const rows = await db.execute<{ waiting: boolean }>( + sql`SELECT EXISTS (SELECT 1 FROM pg_stat_activity WHERE ${blocker} = ANY(pg_blocking_pids(pid))) AS waiting`, + ); + if (rows.rows[0]?.waiting === true) return; + await Bun.sleep(5); + } + throw new Error("decision did not reach the held authorization row"); +} +test.skipIf(!reachable)( + "decisions serialize with current owner, admin role, ban and service identity changes", + async () => { + const f = await reviewFixture(); + try { + const { call, expected } = await pending(f); + const changes = ["owner", "role", "ban", "service"]; + for (const change of changes) { + await f.db + .update(task) + .set({ createdByUserId: change === "role" ? "other-owner" : f.actorId }) + .where(eq(task.id, f.taskId)); + await f.db + .update(user) + .set({ + role: change === "role" ? "admin" : "user", + banned: false, + email: `${f.actorId}@example.test`, + }) + .where(eq(user.id, f.actorId)); + let unlock!: () => void, locked!: (pid: number) => void; + const release = new Promise((resolve) => { + unlock = resolve; + }); + const ready = new Promise((resolve) => { + locked = resolve; + }); + const mutation = f.db.transaction(async (tx) => { + if (change === "owner") + await tx + .update(task) + .set({ createdByUserId: "other-owner" }) + .where(eq(task.id, f.taskId)); + else + await tx + .update(user) + .set( + change === "role" + ? { role: "user" } + : change === "ban" + ? { banned: true } + : { email: `apikey+${f.actorId}@service.local` }, + ) + .where(eq(user.id, f.actorId)); + const rows = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`); + locked(Number(rows.rows[0]!.pid)); + await release; + }); + const blocker = await ready; + const decision = f.journal.decide(call.id, f.actorId, true, expected, f.now).then( + () => "allowed", + () => "denied", + ); + try { + await waitForBlocked(f.db, blocker); + } finally { + unlock(); + } + await mutation; + expect(await decision).toBe("denied"); + expect((await f.journal.get(call.id))!.state).toBe("waiting_approval"); + } + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "local profile changes and replaced bindings invalidate review", + async () => { + const f = await reviewFixture(); + try { + const { call, expected } = await pending(f); + await f.db.update(profile).set({ integrationGrants: [] }).where(eq(profile.id, f.profileId)); + await expect(f.journal.decide(call.id, f.actorId, true, expected, f.now)).rejects.toThrow( + "stale", + ); + } finally { + await f.cleanup(); + } + }, +); +test.skipIf(!reachable)( + "failed barrier releases reservation without consuming approval", + async () => { + const f = await reviewFixture(); + try { + const { call, expected } = await pending(f); + const decision = await f.journal.decide(call.id, f.actorId, true, expected, f.now); + await f.journal.admit(call.id, f.deploymentId, f.now); + const authorization = { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: decision.call.version - 1, + reservationId: (await f.journal.getAdmission(call.id))!.reservationId, + reservationExpiresAt: (await f.journal.getAdmission(call.id))!.expiresAt, + }; + await expect( + f.journal.dispatch( + call.id, + { + bootId: "boot-a", + intentId: crypto.randomUUID(), + expiresAt: new Date(f.now.getTime() + 1000), + }, + f.now, + authorization, + ), + ).rejects.toThrow("stale"); + expect( + await f.db.select().from(mcpAdmission).where(eq(mcpAdmission.executionId, call.id)), + ).toHaveLength(0); + const [approval] = await f.db + .select() + .from(mcpApproval) + .where(eq(mcpApproval.executionId, call.id)); + expect(approval!.consumedAt).toBeNull(); + expect((await f.journal.get(call.id))!.state).toBe("authorized"); + } finally { + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "unknown write blocks are global to the session across state pages", + async () => { + const f = await reviewFixture(); + try { + const { call, expected } = await pending(f); + const approved = await f.journal.decide(call.id, f.actorId, true, expected, f.now); + await f.journal.admit(call.id, f.deploymentId, f.now); + const dispatched = await f.journal.dispatch( + call.id, + { + bootId: "boot-a", + intentId: crypto.randomUUID(), + expiresAt: new Date(f.now.getTime() + 1000), + }, + f.now, + { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: approved.call.version, + reservationId: (await f.journal.getAdmission(call.id))!.reservationId, + reservationExpiresAt: (await f.journal.getAdmission(call.id))!.expiresAt, + }, + ); + await f.journal.finish(call.id, dispatched.version, "outcome_unknown", null, false, f.now); + const page = await f.journal.readSession(f.sessionId, f.actorId, { after: "zzzz" }); + expect(page.rows).toHaveLength(0); + expect(page.unknownWriteBlocks).toEqual([f.connection.id]); + await f.journal.resolveUnknown(call.id, f.actorId, "acknowledged", f.now); + expect((await f.journal.readSession(f.sessionId, f.actorId)).unknownWriteBlocks).toHaveLength( + 0, + ); + expect( + (await f.journal.readSession(f.sessionId, f.actorId)).rows[0]!.resolution?.outcome, + ).toBe("acknowledged"); + } finally { + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "legacy requests cannot gain review authority and frozen read-only requests stay blocked", + async () => { + const f = await reviewFixture(); + try { + const { call } = await pending(f); + const legacyId = crypto.randomUUID(); + await f.db.insert(mcpExecution).values({ + ...call, + id: legacyId, + toolCallId: "legacy", + originalArguments: null, + requestContext: null, + }); + await f.journal.requestApproval(legacyId, f.deploymentId, f.now); + const current = await f.journal.pending(f.sessionId, f.actorId, f.now); + expect(current.map((row) => row.call.id)).not.toContain(legacyId); + await expect( + f.journal.decide( + legacyId, + f.actorId, + true, + reviewContext(call, current[0]!.approval), + f.now, + ), + ).rejects.toThrow("context unavailable"); + const readonly = await f.journal.create( + { + ...f.input, + toolCallId: "read-only", + requestPolicyContext: { + ...f.input.requestPolicyContext, + toolCallId: "read-only", + acceptedWriteEligibility: "read_only", + }, + }, + { kind: "approval" }, + f.now, + ); + const approval = await f.journal.requestApproval(readonly.id, f.deploymentId, f.now); + await expect( + f.journal.decide(readonly.id, f.actorId, true, reviewContext(readonly, approval), f.now), + ).rejects.toThrow("policy denies"); + expect((await f.journal.get(readonly.id))!.state).toBe("waiting_approval"); + } finally { + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "ancestor profile revocation invalidates a child approval without changing the child profile", + async () => { + const f = await reviewFixture(); + const childTaskId = crypto.randomUUID(), + childSessionId = crypto.randomUUID(), + childProfileId = crypto.randomUUID(); + try { + const { compileSessionBindings } = await import("../mcp/session-bindings.ts"); + const { bindingSnapshotColumns } = await import("../mcp/binding-store.ts"); + const live = { ...f.authority, profileId: childProfileId }; + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: childProfileId, integrationGrants: live.profileGrants }, + liveAuthority: live, + bindingEpoch: 0, + requestedGrants: live.profileGrants, + parentAuthority: { sessionId: f.sessionId, snapshot: f.snapshot }, + }); + await f.db.insert(profile).values({ + id: childProfileId, + name: "Child review profile", + imageId: "image", + harness: "claude", + integrationGrants: live.profileGrants, + }); + await f.db.insert(task).values({ + id: childTaskId, + type: "chat", + createdByUserId: f.actorId, + spawningSessionId: f.sessionId, + }); + await f.db.insert(taskSession).values({ + taskId: childTaskId, + sessionId: childSessionId, + ...bindingSnapshotColumns(snapshot, [ + { + id: f.connection.id, + alias: f.connection.alias, + provider: "mcp", + displayName: "Tools", + config: f.connection.config, + }, + ]), + }); + const binding = snapshot.immutableBindings[0]!, + bound = binding.tools[0]!; + const call = await f.journal.create( + { + ...f.input, + taskId: childTaskId, + sessionId: childSessionId, + bindingHash: snapshot.bindingHash, + resolvedTool: { + ...f.input.resolvedTool, + bindingHash: snapshot.bindingHash, + binding, + effectiveGrant: bound.grant, + ancestorAuthorityRefs: snapshot.ancestorAuthorityRefs, + }, + requestPolicyContext: { + ...f.input.requestPolicyContext, + sessionId: childSessionId, + acceptedPolicyRef: { + ...f.input.requestPolicyContext.acceptedPolicyRef, + sessionId: childSessionId, + }, + ancestorPolicyRefs: [f.input.requestPolicyContext.acceptedPolicyRef], + }, + }, + { kind: "approval" }, + f.now, + ); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, f.now); + await f.db.update(profile).set({ integrationGrants: [] }).where(eq(profile.id, f.profileId)); + await expect( + f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), f.now), + ).rejects.toThrow("stale"); + expect((await f.journal.get(call.id))!.state).toBe("waiting_approval"); + } finally { + await f.db.delete(task).where(eq(task.id, childTaskId)); + await f.db.delete(profile).where(eq(profile.id, childProfileId)); + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "competing first requests with different argument bytes cannot share a call", + async () => { + const f = await reviewFixture(); + try { + const inputs = [ + f.input, + { ...f.input, originalArguments: '{"x":2}', argumentHash: canonicalDigest({ x: 2 }) }, + ]; + const outcomes = await Promise.allSettled( + inputs.map((input) => f.journal.create(input, { kind: "approval" }, f.now)), + ); + expect(outcomes.filter((result) => result.status === "fulfilled")).toHaveLength(1); + expect(outcomes.filter((result) => result.status === "rejected")).toHaveLength(1); + const state = await f.journal.readSession(f.sessionId, f.actorId); + expect(state.rows).toHaveLength(1); + const stored = state.rows[0]!.call; + expect(canonicalDigest(JSON.parse(stored.originalArguments!))).toBe(stored.argumentHash); + } finally { + await f.cleanup(); + } + }, +); + +test.skipIf(!reachable)( + "a stale reservation attempt cannot release a replacement at the same clock time", + async () => { + const f = await reviewFixture(); + try { + const { call, expected } = await pending(f); + const approved = await f.journal.decide(call.id, f.actorId, true, expected, f.now); + await f.journal.admit(call.id, f.deploymentId, f.now); + const old = (await f.journal.getAdmission(call.id))!; + const authorization = { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: approved.call.version, + reservationId: old.reservationId, + reservationExpiresAt: old.expiresAt, + }; + const intent = { + bootId: "boot-a", + intentId: crypto.randomUUID(), + expiresAt: new Date(f.now.getTime() + 1000), + }; + await expect( + f.journal.dispatch(call.id, intent, f.now, { + ...authorization, + expectedJournalRevision: 0, + }), + ).rejects.toThrow(); + await f.journal.admit(call.id, f.deploymentId, f.now); + const replacement = (await f.journal.getAdmission(call.id))!; + expect(replacement.createdAt).toEqual(old.createdAt); + expect(replacement.expiresAt).toEqual(old.expiresAt); + expect(replacement.reservationId).not.toBe(old.reservationId); + await expect(f.journal.dispatch(call.id, intent, f.now, authorization)).rejects.toThrow(); + expect((await f.journal.getAdmission(call.id))?.reservationId).toBe( + replacement.reservationId, + ); + } finally { + await f.cleanup(); + } + }, +); diff --git a/orchestrator/src/__tests__/mcp-rpc.test.ts b/orchestrator/src/__tests__/mcp-rpc.test.ts index 9dd92dec4..46050723f 100644 --- a/orchestrator/src/__tests__/mcp-rpc.test.ts +++ b/orchestrator/src/__tests__/mcp-rpc.test.ts @@ -66,6 +66,8 @@ function services(): McpServices { }, journal: { get: unavailable, + readSession: unavailable, + getAdmission: unavailable, hasUnknown: unavailable, create: unavailable, requestApproval: unavailable, diff --git a/orchestrator/src/__tests__/mcp-store.test.ts b/orchestrator/src/__tests__/mcp-store.test.ts index e6f44d521..a40eff3d8 100644 --- a/orchestrator/src/__tests__/mcp-store.test.ts +++ b/orchestrator/src/__tests__/mcp-store.test.ts @@ -1,20 +1,11 @@ import { describe, expect, test } from "bun:test"; -import { eq, inArray, sql } from "drizzle-orm"; -import { checkDb, getDb } from "../db/client.ts"; -import { makeIntegrationConnectionStore } from "../db/integration-connections.ts"; -import { - integrationConnection, - mcpAdmission, - mcpAdmissionScope, - mcpApproval, - mcpCatalog, - mcpExecution, - mcpResolution, - task, - taskSession, -} from "../db/schema.ts"; -import { makeMcpRepository } from "../mcp/repository.ts"; -import { makeMcpJournal } from "../mcp/journal.ts"; +import { eq, sql } from "drizzle-orm"; +import { checkDb } from "../db/client.ts"; +import { mcpAdmission, mcpApproval, task, taskSession } from "../db/schema.ts"; +import { reviewFixture } from "./mcp-review-fixture.ts"; +import { bindingSnapshotColumns } from "../mcp/binding-store.ts"; +import { reviewContext } from "../mcp/journal-review.ts"; +import { type McpCallAuthorization, makeMcpJournal } from "../mcp/journal.ts"; import { canonicalDigest } from "../mcp/hash.ts"; const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; @@ -28,80 +19,80 @@ const config = { zone: "public", auth: { kind: "api_key", secretRef: "mcp.service.api-key" }, }; +// These adapters supply trusted producer fixtures only. They do not prove host or runtime enforcement. async function fixture() { - const db = getDb(); - const connections = makeIntegrationConnectionStore(db); - const row = await connections.create({ - alias: `mcp-${crypto.randomUUID()}`, - provider: "mcp", - displayName: "Tools", - config, - }); - const taskId = crypto.randomUUID(); - const sessionId = crypto.randomUUID(); - await db.insert(task).values({ id: taskId, type: "chat", createdByUserId: "owner" }); - await db.insert(taskSession).values({ taskId, sessionId }); - const repo = makeMcpRepository(db); - const now = new Date("2026-01-01T00:00:00Z"); - await repo.setAuthorization(row.id, 1, 1, "account-a", now); - const live = (await connections.get(row.id))!; - const draft = await repo.saveDraft(row.id, live.mcp!, [tool], "admin", now); - const published = await repo.publish( - row.id, - draft.id, - 0, - [{ ...tool, classification: "write", enabled: true }], - "admin", - now, - ); - await repo.setEnabled(row.id, 1, true, now); - const journal = makeMcpJournal(db); - const deploymentId = crypto.randomUUID(); - const input = { - taskId, - sessionId, - runId: "run-1", - toolCallId: "call-1", - connectionId: row.id, - catalogId: published.id, - principalId: "owner", - bindingHash: canonicalDigest({ binding: 1 }), - argumentHash: canonicalDigest({ x: 1 }), - upstreamName: tool.upstreamName, - configRevision: 1, - authGeneration: live.mcp!.authGeneration, - classification: "write" as const, - deadline: new Date(now.getTime() + 60_000), - }; - return { - db, - connections, - row, - repo, - now, - draft, - published, - journal, - input, - deploymentId, - async cleanup() { - const executions = await db - .select({ id: mcpExecution.id }) - .from(mcpExecution) - .where(eq(mcpExecution.sessionId, sessionId)); - const ids = executions.map((e) => e.id); - if (ids.length) { - await db.delete(mcpAdmission).where(inArray(mcpAdmission.executionId, ids)); - await db.delete(mcpApproval).where(inArray(mcpApproval.executionId, ids)); - await db.delete(mcpResolution).where(inArray(mcpResolution.executionId, ids)); - await db.delete(mcpExecution).where(inArray(mcpExecution.id, ids)); - } - await db.delete(mcpAdmissionScope).where(eq(mcpAdmissionScope.id, deploymentId)); - await db.delete(mcpCatalog).where(eq(mcpCatalog.connectionId, row.id)); - await db.delete(task).where(eq(task.id, taskId)); - await connections.delete(row.id); + const f = await reviewFixture(); + const base = f.journal; + const input = { ...f.input, deadline: new Date(f.now.getTime() + 60_000) }; + const actor = (id: string) => (id === "owner" ? f.actorId : id); + const journal = { + ...base, + async create(value: typeof input, authorization: McpCallAuthorization, now: Date) { + const { sessionId, runId, toolCallId } = value; + // Additional sessions in capacity tests use the same persisted fixture authority. + await f.db + .update(taskSession) + .set( + bindingSnapshotColumns(f.snapshot, [ + { + id: f.connection.id, + alias: f.connection.alias, + provider: "mcp", + displayName: f.connection.displayName, + config: f.connection.config, + }, + ]), + ) + .where(eq(taskSession.sessionId, sessionId)); + return base.create( + { + ...value, + requestPolicyContext: { + ...value.requestPolicyContext, + sessionId, + runId, + toolCallId, + acceptedPolicyRef: { ...value.requestPolicyContext.acceptedPolicyRef, sessionId }, + }, + }, + authorization, + now, + ); + }, + pending: (sid: string, id: string, _cachedAdmin: boolean, now: Date) => + base.pending(sid, actor(id), now), + async decide(id: string, who: string, _cachedAdmin: boolean, approve: boolean, now: Date) { + const call = (await base.get(id))!; + const [item] = await f.db.select().from(mcpApproval).where(eq(mcpApproval.executionId, id)); + return base.decide(id, actor(who), approve, reviewContext(call, item!), now); + }, + async dispatch( + id: string, + intent: { bootId: string; intentId: string; expiresAt: Date }, + now: Date, + ) { + const call = (await base.get(id))!; + return base.dispatch(id, intent, now, { + executionId: id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: call.version, + reservationId: (await base.getAdmission(id))!.reservationId, + reservationExpiresAt: (await base.getAdmission(id))!.expiresAt, + }); }, + cancel: (id: string, who: string, _cachedAdmin: boolean, now: Date) => + base.cancel(id, actor(who), now), + resolveUnknown: ( + id: string, + who: string, + _cachedAdmin: boolean, + outcome: "succeeded" | "failed" | "acknowledged", + now: Date, + ) => base.resolveUnknown(id, actor(who), outcome, now), }; + return { ...f, input, journal, row: f.connection, repo: f.repository, published: f.catalog }; } describe("MCP durable authority", () => { @@ -178,6 +169,7 @@ describe("MCP durable authority", () => { expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(1); const current = (await f.journal.get(call.id))!; expect(current.state).toBe("dispatching"); + expect((await f.journal.getAdmission(call.id))?.state).toBe("admitted"); await f.journal.finish(call.id, current.version, "outcome_unknown", null, false, f.now); const next = await f.journal.create( { ...f.input, toolCallId: "call-2" }, @@ -226,8 +218,8 @@ describe("MCP durable authority", () => { const calls = await Promise.all( Array.from({ length: 6 }, (_, n) => f.journal.create( - { ...f.input, toolCallId: `call-${n}`, classification: "read" }, - { kind: "read" }, + { ...f.input, toolCallId: `call-${n}` }, + { kind: "automation", grant: f.input.resolvedTool.effectiveGrant }, f.now, ), ), @@ -278,25 +270,14 @@ describe("MCP authority isolation", () => { first.now, ), ).rejects.toThrow("explicit automation"); - const call = await first.journal.create( - { ...first.input, catalogId: second.published.id }, - { kind: "approval" }, - first.now, - ); - await first.journal.requestApproval(call.id, first.deploymentId, first.now); - await first.journal.decide(call.id, "owner", false, true, first.now); - await first.journal.admit(call.id, first.deploymentId, first.now); await expect( - first.journal.dispatch( - call.id, - { - bootId: "boot", - intentId: crypto.randomUUID(), - expiresAt: new Date(first.now.getTime() + 1000), - }, + first.journal.create( + { ...first.input, catalogId: second.published.id }, + { kind: "approval" }, first.now, ), - ).rejects.toThrow("stale"); + ).rejects.toThrow("identity"); + const call = await first.journal.create(first.input, { kind: "approval" }, first.now); await expect( Promise.resolve( first.db.execute( @@ -313,7 +294,7 @@ describe("MCP authority isolation", () => { ).rejects.toThrow(); await expect( first.journal.pending(first.input.sessionId, "intruder", false, first.now), - ).resolves.toHaveLength(0); + ).rejects.toThrow("owner"); } finally { await first.cleanup(); await second.cleanup(); @@ -467,7 +448,7 @@ test.skipIf(!reachable)("replicas share connection and deployment admission ceil for (const f of fixtures) await f.db.delete(task).where(eq(task.id, f.input.taskId)); await first.db .insert(task) - .values({ id: first.input.taskId, type: "chat", createdByUserId: "owner" }); + .values({ id: first.input.taskId, type: "chat", createdByUserId: first.actorId }); const sessions = [first.input.sessionId, crypto.randomUUID(), crypto.randomUUID()]; for (const sessionId of sessions) await first.db.insert(taskSession).values({ taskId: first.input.taskId, sessionId }); diff --git a/orchestrator/src/db/schema.ts b/orchestrator/src/db/schema.ts index 47ceb3ef0..04bbbc835 100644 --- a/orchestrator/src/db/schema.ts +++ b/orchestrator/src/db/schema.ts @@ -2224,6 +2224,8 @@ export const mcpExecution = pgTable( bindingHash: text("binding_hash").notNull(), requestDigest: text("request_digest").notNull(), argumentHash: text("argument_hash").notNull(), + originalArguments: text("original_arguments"), + requestContext: jsonb("request_context").$type(), upstreamName: text("upstream_name").notNull(), configRevision: integer("config_revision").notNull(), authGeneration: integer("auth_generation").notNull(), @@ -2325,6 +2327,7 @@ export const mcpAdmission = pgTable( executionId: text("execution_id") .primaryKey() .references(() => mcpExecution.id, { onDelete: "cascade" }), + reservationId: text("reservation_id").notNull().default(sql`gen_random_uuid()::text`), deploymentId: text("deployment_id") .notNull() .references(() => mcpAdmissionScope.id), @@ -2337,6 +2340,7 @@ export const mcpAdmission = pgTable( createdAt: timestamp("created_at", { withTimezone: true }).notNull(), }, (t) => [ + uniqueIndex("mcp_admission_reservation_idx").on(t.reservationId), index("mcp_admission_deployment_idx").on(t.deploymentId, t.state, t.expiresAt), index("mcp_admission_connection_idx").on(t.connectionId, t.state), index("mcp_admission_session_idx").on(t.sessionId, t.state), diff --git a/orchestrator/src/gen/engram/app/v1/integration_pb.ts b/orchestrator/src/gen/engram/app/v1/integration_pb.ts index 5d5c0784f..d0c59bd2b 100644 --- a/orchestrator/src/gen/engram/app/v1/integration_pb.ts +++ b/orchestrator/src/gen/engram/app/v1/integration_pb.ts @@ -4,13 +4,15 @@ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GetMcpWritePolicyResponse } from "./session_pb"; +import { file_engram_app_v1_session } from "./session_pb"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file engram/app/v1/integration.proto. */ export const file_engram_app_v1_integration: GenFile = /*@__PURE__*/ - fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIs4BCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkiLQoXTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJJChhMaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USLQoJYXBwcm92YWxzGAEgAygLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbCJBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgiKgoZRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRINCgVzdGF0ZRgBIAEoCSIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAkilQEKF1B1dE1jcENyZWRlbnRpYWxSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAiABKA0SGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgDIAEoDRIMCgRraW5kGAQgASgJEg4KBmhlYWRlchgFIAEoCRINCgV2YWx1ZRgGIAEoCSKUAQoTTWNwQ3JlZGVudGlhbFN0YXR1cxIOCgZzdGF0dXMYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJEgwKBGtpbmQYAyABKAkSGwoTY3JlZGVudGlhbF9pZGVudGl0eRgEIAEoCRIPCgd2ZXJzaW9uGAUgASgDEh4KFnJlc2VydmF0aW9uX2dlbmVyYXRpb24YBiABKAMiiQEKFU1jcENyZWRlbnRpYWxSZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyI7ChZNY3BBcHByb3ZlZERlc3RpbmF0aW9uEg8KB3pvbmVfaWQYASABKAkSEAoIZW5kcG9pbnQYAiABKAkiuwEKFU1jcENsaWVudFJlZ2lzdHJhdGlvbhIKCgJpZBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSFAoMY29uZmlkZW50aWFsGAMgASgIEhQKDHJlZGlyZWN0X3VyaRgEIAEoCRIOCgZpc3N1ZXIYBSABKAkSNwoIcmVzb3VyY2UYBiABKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SDgoGc2NvcGVzGAcgAygJIisKEkdldE1jcFNldHVwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJIr8CChNHZXRNY3BTZXR1cFJlc3BvbnNlEg8KB2VuYWJsZWQYASABKAgSOwoNcmVnaXN0cmF0aW9ucxgCIAMoCzIkLmVuZ3JhbS5hcHAudjEuTWNwQ2xpZW50UmVnaXN0cmF0aW9uEjsKDGRlc3RpbmF0aW9ucxgDIAMoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92ZWREZXN0aW5hdGlvbhI1CgdjdXJyZW50GAQgASgLMiQuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsUmVzcG9uc2USGgoSdHJhbnNpdGlvbl9wZW5kaW5nGAUgASgIEg8KB2Zsb3dfaWQYBiABKAkSFwoPZmxvd19leHBpcmVzX2F0GAcgASgJEiAKGGNsaWVudF9zZWNyZXRfY29uZmlndXJlZBgIIAEoCCLUAQofQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDwoHZmxvd19pZBgEIAEoCRINCgVzdGF0ZRgFIAEoCRIMCgRjb2RlGAYgASgJEhMKBmlzc3VlchgHIAEoCUgAiAEBEhQKDHJlZGlyZWN0X3VyaRgIIAEoCUIJCgdfaXNzdWVyImwKG1JlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0y8BUKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USYAoQUHV0TWNwQ3JlZGVudGlhbBImLmVuZ3JhbS5hcHAudjEuUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxSZXNwb25zZRJUCgtHZXRNY3BTZXR1cBIhLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRNY3BTZXR1cFJlc3BvbnNlEnAKGENvbXBsZXRlTWNwQXV0aG9yaXphdGlvbhIuLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFJlc3BvbnNlEmgKFFJlY292ZXJNY3BDcmVkZW50aWFsEiouZW5ncmFtLmFwcC52MS5SZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxSZXNwb25zZRJXCgxBdXRob3JpemVNY3ASIi5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlc3BvbnNlEloKDURpc2Nvbm5lY3RNY3ASIy5lbmdyYW0uYXBwLnYxLkRpc2Nvbm5lY3RNY3BSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVzcG9uc2USYwoQTGlzdE1jcEFwcHJvdmFscxImLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRJmChFEZWNpZGVNY3BBcHByb3ZhbBInLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEloKDUNhbmNlbE1jcENhbGwSIy5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVzcG9uc2USZgoRUmVzb2x2ZU1jcE91dGNvbWUSJy5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRJOCglSZWJpbmRNY3ASHy5lbmdyYW0uYXBwLnYxLlJlYmluZE1jcFJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLlJlYmluZE1jcFJlc3BvbnNlYgZwcm90bzM"); + fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIo8DCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkSGgoSb3JpZ2luYWxfYXJndW1lbnRzGAogASgJEi0KDXJldmlld2VkX3Rvb2wYCyABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgMIAMoCRJIChdleHBlY3RlZF9yZXZpZXdfY29udGV4dBgNIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IkkKF0xpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSGgoSYWZ0ZXJfZXhlY3V0aW9uX2lkGAIgASgJImQKGExpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRItCglhcHByb3ZhbHMYASADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhkKEW5leHRfZXhlY3V0aW9uX2lkGAIgASgJIosBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgSSAoXZXhwZWN0ZWRfcmV2aWV3X2NvbnRleHQYAyABKAsyJy5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dCJeChlEZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJEjIKB3JlY2VpcHQYAiABKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmVjZWlwdCIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAkilQEKF1B1dE1jcENyZWRlbnRpYWxSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAiABKA0SGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgDIAEoDRIMCgRraW5kGAQgASgJEg4KBmhlYWRlchgFIAEoCRINCgV2YWx1ZRgGIAEoCSKUAQoTTWNwQ3JlZGVudGlhbFN0YXR1cxIOCgZzdGF0dXMYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJEgwKBGtpbmQYAyABKAkSGwoTY3JlZGVudGlhbF9pZGVudGl0eRgEIAEoCRIPCgd2ZXJzaW9uGAUgASgDEh4KFnJlc2VydmF0aW9uX2dlbmVyYXRpb24YBiABKAMijAEKGFB1dE1jcENyZWRlbnRpYWxSZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyKUAQogQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMikAEKHFJlY292ZXJNY3BDcmVkZW50aWFsUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMihgEKEk1jcENyZWRlbnRpYWxTdGF0ZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyI7ChZNY3BBcHByb3ZlZERlc3RpbmF0aW9uEg8KB3pvbmVfaWQYASABKAkSEAoIZW5kcG9pbnQYAiABKAkiuwEKFU1jcENsaWVudFJlZ2lzdHJhdGlvbhIKCgJpZBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSFAoMY29uZmlkZW50aWFsGAMgASgIEhQKDHJlZGlyZWN0X3VyaRgEIAEoCRIOCgZpc3N1ZXIYBSABKAkSNwoIcmVzb3VyY2UYBiABKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SDgoGc2NvcGVzGAcgAygJIisKEkdldE1jcFNldHVwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJIrwCChNHZXRNY3BTZXR1cFJlc3BvbnNlEg8KB2VuYWJsZWQYASABKAgSOwoNcmVnaXN0cmF0aW9ucxgCIAMoCzIkLmVuZ3JhbS5hcHAudjEuTWNwQ2xpZW50UmVnaXN0cmF0aW9uEjsKDGRlc3RpbmF0aW9ucxgDIAMoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92ZWREZXN0aW5hdGlvbhIyCgdjdXJyZW50GAQgASgLMiEuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdGUSGgoSdHJhbnNpdGlvbl9wZW5kaW5nGAUgASgIEg8KB2Zsb3dfaWQYBiABKAkSFwoPZmxvd19leHBpcmVzX2F0GAcgASgJEiAKGGNsaWVudF9zZWNyZXRfY29uZmlndXJlZBgIIAEoCCLUAQofQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDwoHZmxvd19pZBgEIAEoCRINCgVzdGF0ZRgFIAEoCRIMCgRjb2RlGAYgASgJEhMKBmlzc3VlchgHIAEoCUgAiAEBEhQKDHJlZGlyZWN0X3VyaRgIIAEoCUIJCgdfaXNzdWVyImwKG1JlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0i6gMKGE1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhYKDnJlcXVlc3RfZGlnZXN0GAUgASgJEhUKDWFyZ3VtZW50X2hhc2gYBiABKAkSFAoMYmluZGluZ19oYXNoGAcgASgJEhUKDWNvbm5lY3Rpb25faWQYCCABKAkSEgoKY2F0YWxvZ19pZBgJIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGAogASgNEhcKD2NvbmZpZ19yZXZpc2lvbhgLIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDCABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgNIAEoCRIVCg11cHN0cmVhbV9uYW1lGA4gASgJEhgKEHRvb2xfZmluZ2VycHJpbnQYDyABKAkSFgoOY29udGV4dF9kaWdlc3QYECABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgRIAEoDRISCgpleHBpcmVzX2F0GBIgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgTIAEoCRIVCg1iaW5kaW5nX2Vwb2NoGBQgASgJIr8BChJNY3BBcHByb3ZhbFJlY2VpcHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhAKCGRlY2lzaW9uGAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKCmRlY2lkZWRfYXQYBCABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgFIAEoDRJBChByZXZpZXdlZF9jb250ZXh0GAYgASgLMicuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbFJldmlld0NvbnRleHQi2AMKEk1jcEV4ZWN1dGlvblJldmlldxIUCgxleGVjdXRpb25faWQYASABKAkSFQoNY29ubmVjdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRIYChBqb3VybmFsX3JldmlzaW9uGAQgASgNEhoKEnJlc3VsdF91bmF2YWlsYWJsZRgFIAEoCBIqCgZyZXZpZXcYBiABKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhcKD2FsbG93ZWRfYWN0aW9ucxgHIAMoCRIOCgZyZWFzb24YCCABKAkSFAoMdG9vbF9jYWxsX2lkGAkgASgJEg4KBnJ1bl9pZBgKIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgLIAEoCRIVCg11cHN0cmVhbV9uYW1lGAwgASgJEhkKEWFwcHJvdmFsX2RlY2lzaW9uGA0gASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgOIAEoCRItCg1yZXZpZXdlZF90b29sGA8gASgLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YECADKAkSFAoMYmluZGluZ19oYXNoGBEgASgJImMKFE1jcFJlc29sdXRpb25SZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRIPCgdvdXRjb21lGAMgASgJEhIKCmNyZWF0ZWRfYXQYBCABKAkigQEKEU1jcEJpbmRpbmdSZWNlaXB0EhQKDGJpbmRpbmdfaGFzaBgBIAEoCRIVCg1iaW5kaW5nX2Vwb2NoGAIgASgJEg8KB2N1cnJlbnQYAyABKAgSFQoNc3RhbGVfcmVhc29ucxgEIAMoCRIXCg9hbGxvd2VkX2FjdGlvbnMYBSADKAkigAIKGUdldFNlc3Npb25NY3BTdGF0ZVJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIaChJhZnRlcl9leGVjdXRpb25faWQYAiABKAkSEQoJcGFnZV9zaXplGAMgASgNEh4KEXBsYW5fdG9vbF9jYWxsX2lkGAQgASgJSACIAQESHwoXYWZ0ZXJfYmluZGluZ190b29sX25hbWUYBSABKAkSGQoRYmluZGluZ19wYWdlX3NpemUYBiABKA0SFQoNb21pdF9iaW5kaW5ncxgHIAEoCBIXCg9vbWl0X2V4ZWN1dGlvbnMYCCABKAhCFAoSX3BsYW5fdG9vbF9jYWxsX2lkIr8EChpHZXRTZXNzaW9uTWNwU3RhdGVSZXNwb25zZRI5Cg9iaW5kaW5nX3JlY2VpcHQYASABKAsyIC5lbmdyYW0uYXBwLnYxLk1jcEJpbmRpbmdSZWNlaXB0EjUKCmV4ZWN1dGlvbnMYAiADKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEV4ZWN1dGlvblJldmlldxItCglhcHByb3ZhbHMYAyADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhwKFHVua25vd25fd3JpdGVfYmxvY2tzGAQgAygJEkAKE3Jlc29sdXRpb25fcmVjZWlwdHMYBSADKAsyIy5lbmdyYW0uYXBwLnYxLk1jcFJlc29sdXRpb25SZWNlaXB0EhcKD2FsbG93ZWRfYWN0aW9ucxgGIAMoCRIUCgxhdmFpbGFiaWxpdHkYByABKAkSGQoRbmV4dF9leGVjdXRpb25faWQYCCABKAkSPgoMd3JpdGVfcG9saWN5GAkgASgLMiguZW5ncmFtLmFwcC52MS5HZXRNY3BXcml0ZVBvbGljeVJlc3BvbnNlEjkKDWJpbmRpbmdfdG9vbHMYCiADKAsyIi5lbmdyYW0uYXBwLnYxLk1jcEJvdW5kVG9vbFN1bW1hcnkSHgoWbmV4dF9iaW5kaW5nX3Rvb2xfbmFtZRgLIAEoCRIeChZoYXNfbW9yZV9iaW5kaW5nX3Rvb2xzGAwgASgIEhsKE2hhc19tb3JlX2V4ZWN1dGlvbnMYDSABKAgiygIKE01jcEJvdW5kVG9vbFN1bW1hcnkSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIYChBjb25uZWN0aW9uX2FsaWFzGAIgASgJEhcKD21vZGVsX3Rvb2xfbmFtZRgDIAEoCRItCg1yZXZpZXdlZF90b29sGAQgASgLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YBSADKAkSGAoQYXV0b21hdGlvbl93cml0ZRgGIAEoCBIUCgx1bnJlc3RyaWN0ZWQYByABKAgSEgoKY2F0YWxvZ19pZBgIIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGAkgASgNEhcKD2NvbmZpZ19yZXZpc2lvbhgKIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YCyABKA0y8BYKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USYwoQUHV0TWNwQ3JlZGVudGlhbBImLmVuZ3JhbS5hcHAudjEuUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLlB1dE1jcENyZWRlbnRpYWxSZXNwb25zZRJUCgtHZXRNY3BTZXR1cBIhLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRNY3BTZXR1cFJlc3BvbnNlEnsKGENvbXBsZXRlTWNwQXV0aG9yaXphdGlvbhIuLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBovLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVzcG9uc2USbwoUUmVjb3Zlck1jcENyZWRlbnRpYWwSKi5lbmdyYW0uYXBwLnYxLlJlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuUmVjb3Zlck1jcENyZWRlbnRpYWxSZXNwb25zZRJXCgxBdXRob3JpemVNY3ASIi5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlc3BvbnNlEloKDURpc2Nvbm5lY3RNY3ASIy5lbmdyYW0uYXBwLnYxLkRpc2Nvbm5lY3RNY3BSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVzcG9uc2USaQoSR2V0U2Vzc2lvbk1jcFN0YXRlEiguZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXF1ZXN0GikuZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXNwb25zZRJjChBMaXN0TWNwQXBwcm92YWxzEiYuZW5ncmFtLmFwcC52MS5MaXN0TWNwQXBwcm92YWxzUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1Jlc3BvbnNlEmYKEURlY2lkZU1jcEFwcHJvdmFsEicuZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLkRlY2lkZU1jcEFwcHJvdmFsUmVzcG9uc2USWgoNQ2FuY2VsTWNwQ2FsbBIjLmVuZ3JhbS5hcHAudjEuQ2FuY2VsTWNwQ2FsbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXNwb25zZRJmChFSZXNvbHZlTWNwT3V0Y29tZRInLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5SZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEk4KCVJlYmluZE1jcBIfLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVzcG9uc2ViBnByb3RvMw", [file_engram_app_v1_session]); /** * @generated from message engram.app.v1.GoogleCloudConnectionConfig @@ -1493,6 +1495,26 @@ export type McpApproval = Message<"engram.app.v1.McpApproval"> & { * @generated from field: string expires_at = 9; */ expiresAt: string; + + /** + * @generated from field: string original_arguments = 10; + */ + originalArguments: string; + + /** + * @generated from field: engram.app.v1.McpTool reviewed_tool = 11; + */ + reviewedTool?: McpTool; + + /** + * @generated from field: repeated string argument_constraint_documents_json = 12; + */ + argumentConstraintDocumentsJson: string[]; + + /** + * @generated from field: engram.app.v1.McpApprovalReviewContext expected_review_context = 13; + */ + expectedReviewContext?: McpApprovalReviewContext; }; /** @@ -1510,6 +1532,11 @@ export type ListMcpApprovalsRequest = Message<"engram.app.v1.ListMcpApprovalsReq * @generated from field: string session_id = 1; */ sessionId: string; + + /** + * @generated from field: string after_execution_id = 2; + */ + afterExecutionId: string; }; /** @@ -1527,6 +1554,11 @@ export type ListMcpApprovalsResponse = Message<"engram.app.v1.ListMcpApprovalsRe * @generated from field: repeated engram.app.v1.McpApproval approvals = 1; */ approvals: McpApproval[]; + + /** + * @generated from field: string next_execution_id = 2; + */ + nextExecutionId: string; }; /** @@ -1549,6 +1581,11 @@ export type DecideMcpApprovalRequest = Message<"engram.app.v1.DecideMcpApprovalR * @generated from field: bool approve = 2; */ approve: boolean; + + /** + * @generated from field: engram.app.v1.McpApprovalReviewContext expected_review_context = 3; + */ + expectedReviewContext?: McpApprovalReviewContext; }; /** @@ -1566,6 +1603,11 @@ export type DecideMcpApprovalResponse = Message<"engram.app.v1.DecideMcpApproval * @generated from field: string state = 1; */ state: string; + + /** + * @generated from field: engram.app.v1.McpApprovalReceipt receipt = 2; + */ + receipt?: McpApprovalReceipt; }; /** @@ -1774,9 +1816,9 @@ export const McpCredentialStatusSchema: GenMessage = /*@__P messageDesc(file_engram_app_v1_integration, 61); /** - * @generated from message engram.app.v1.McpCredentialResponse + * @generated from message engram.app.v1.PutMcpCredentialResponse */ -export type McpCredentialResponse = Message<"engram.app.v1.McpCredentialResponse"> & { +export type PutMcpCredentialResponse = Message<"engram.app.v1.PutMcpCredentialResponse"> & { /** * @generated from field: engram.app.v1.IntegrationConnection connection = 1; */ @@ -1789,12 +1831,78 @@ export type McpCredentialResponse = Message<"engram.app.v1.McpCredentialResponse }; /** - * Describes the message engram.app.v1.McpCredentialResponse. - * Use `create(McpCredentialResponseSchema)` to create a new message. + * Describes the message engram.app.v1.PutMcpCredentialResponse. + * Use `create(PutMcpCredentialResponseSchema)` to create a new message. */ -export const McpCredentialResponseSchema: GenMessage = /*@__PURE__*/ +export const PutMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_integration, 62); +/** + * @generated from message engram.app.v1.CompleteMcpAuthorizationResponse + */ +export type CompleteMcpAuthorizationResponse = Message<"engram.app.v1.CompleteMcpAuthorizationResponse"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; + + /** + * @generated from field: engram.app.v1.McpCredentialStatus credential = 2; + */ + credential?: McpCredentialStatus; +}; + +/** + * Describes the message engram.app.v1.CompleteMcpAuthorizationResponse. + * Use `create(CompleteMcpAuthorizationResponseSchema)` to create a new message. + */ +export const CompleteMcpAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 63); + +/** + * @generated from message engram.app.v1.RecoverMcpCredentialResponse + */ +export type RecoverMcpCredentialResponse = Message<"engram.app.v1.RecoverMcpCredentialResponse"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; + + /** + * @generated from field: engram.app.v1.McpCredentialStatus credential = 2; + */ + credential?: McpCredentialStatus; +}; + +/** + * Describes the message engram.app.v1.RecoverMcpCredentialResponse. + * Use `create(RecoverMcpCredentialResponseSchema)` to create a new message. + */ +export const RecoverMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 64); + +/** + * @generated from message engram.app.v1.McpCredentialState + */ +export type McpCredentialState = Message<"engram.app.v1.McpCredentialState"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; + + /** + * @generated from field: engram.app.v1.McpCredentialStatus credential = 2; + */ + credential?: McpCredentialStatus; +}; + +/** + * Describes the message engram.app.v1.McpCredentialState. + * Use `create(McpCredentialStateSchema)` to create a new message. + */ +export const McpCredentialStateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 65); + /** * @generated from message engram.app.v1.McpApprovedDestination */ @@ -1815,7 +1923,7 @@ export type McpApprovedDestination = Message<"engram.app.v1.McpApprovedDestinati * Use `create(McpApprovedDestinationSchema)` to create a new message. */ export const McpApprovedDestinationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 63); + messageDesc(file_engram_app_v1_integration, 66); /** * @generated from message engram.app.v1.McpClientRegistration @@ -1862,7 +1970,7 @@ export type McpClientRegistration = Message<"engram.app.v1.McpClientRegistration * Use `create(McpClientRegistrationSchema)` to create a new message. */ export const McpClientRegistrationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 64); + messageDesc(file_engram_app_v1_integration, 67); /** * @generated from message engram.app.v1.GetMcpSetupRequest @@ -1879,7 +1987,7 @@ export type GetMcpSetupRequest = Message<"engram.app.v1.GetMcpSetupRequest"> & { * Use `create(GetMcpSetupRequestSchema)` to create a new message. */ export const GetMcpSetupRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 65); + messageDesc(file_engram_app_v1_integration, 68); /** * @generated from message engram.app.v1.GetMcpSetupResponse @@ -1901,9 +2009,9 @@ export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & destinations: McpApprovedDestination[]; /** - * @generated from field: engram.app.v1.McpCredentialResponse current = 4; + * @generated from field: engram.app.v1.McpCredentialState current = 4; */ - current?: McpCredentialResponse; + current?: McpCredentialState; /** * @generated from field: bool transition_pending = 5; @@ -1931,7 +2039,7 @@ export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & * Use `create(GetMcpSetupResponseSchema)` to create a new message. */ export const GetMcpSetupResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 66); + messageDesc(file_engram_app_v1_integration, 69); /** * @generated from message engram.app.v1.CompleteMcpAuthorizationRequest @@ -1983,7 +2091,7 @@ export type CompleteMcpAuthorizationRequest = Message<"engram.app.v1.CompleteMcp * Use `create(CompleteMcpAuthorizationRequestSchema)` to create a new message. */ export const CompleteMcpAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 67); + messageDesc(file_engram_app_v1_integration, 70); /** * @generated from message engram.app.v1.RecoverMcpCredentialRequest @@ -2010,7 +2118,527 @@ export type RecoverMcpCredentialRequest = Message<"engram.app.v1.RecoverMcpCrede * Use `create(RecoverMcpCredentialRequestSchema)` to create a new message. */ export const RecoverMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 68); + messageDesc(file_engram_app_v1_integration, 71); + +/** + * Exact server review. Clients must return it unchanged when they decide. + * + * @generated from message engram.app.v1.McpApprovalReviewContext + */ +export type McpApprovalReviewContext = Message<"engram.app.v1.McpApprovalReviewContext"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 4; + */ + toolCallId: string; + + /** + * @generated from field: string request_digest = 5; + */ + requestDigest: string; + + /** + * @generated from field: string argument_hash = 6; + */ + argumentHash: string; + + /** + * @generated from field: string binding_hash = 7; + */ + bindingHash: string; + + /** + * @generated from field: string connection_id = 8; + */ + connectionId: string; + + /** + * @generated from field: string catalog_id = 9; + */ + catalogId: string; + + /** + * @generated from field: uint32 catalog_revision = 10; + */ + catalogRevision: number; + + /** + * @generated from field: uint32 config_revision = 11; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 12; + */ + authGeneration: number; + + /** + * @generated from field: string credential_identity = 13; + */ + credentialIdentity: string; + + /** + * @generated from field: string upstream_name = 14; + */ + upstreamName: string; + + /** + * @generated from field: string tool_fingerprint = 15; + */ + toolFingerprint: string; + + /** + * @generated from field: string context_digest = 16; + */ + contextDigest: string; + + /** + * @generated from field: uint32 journal_revision = 17; + */ + journalRevision: number; + + /** + * @generated from field: string expires_at = 18; + */ + expiresAt: string; + + /** + * @generated from field: string policy_revision = 19; + */ + policyRevision: string; + + /** + * @generated from field: string binding_epoch = 20; + */ + bindingEpoch: string; +}; + +/** + * Describes the message engram.app.v1.McpApprovalReviewContext. + * Use `create(McpApprovalReviewContextSchema)` to create a new message. + */ +export const McpApprovalReviewContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 72); + +/** + * @generated from message engram.app.v1.McpApprovalReceipt + */ +export type McpApprovalReceipt = Message<"engram.app.v1.McpApprovalReceipt"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string decision = 2; + */ + decision: string; + + /** + * @generated from field: string actor_id = 3; + */ + actorId: string; + + /** + * @generated from field: string decided_at = 4; + */ + decidedAt: string; + + /** + * @generated from field: uint32 journal_revision = 5; + */ + journalRevision: number; + + /** + * @generated from field: engram.app.v1.McpApprovalReviewContext reviewed_context = 6; + */ + reviewedContext?: McpApprovalReviewContext; +}; + +/** + * Describes the message engram.app.v1.McpApprovalReceipt. + * Use `create(McpApprovalReceiptSchema)` to create a new message. + */ +export const McpApprovalReceiptSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 73); + +/** + * @generated from message engram.app.v1.McpExecutionReview + */ +export type McpExecutionReview = Message<"engram.app.v1.McpExecutionReview"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string connection_id = 2; + */ + connectionId: string; + + /** + * @generated from field: string state = 3; + */ + state: string; + + /** + * @generated from field: uint32 journal_revision = 4; + */ + journalRevision: number; + + /** + * @generated from field: bool result_unavailable = 5; + */ + resultUnavailable: boolean; + + /** + * @generated from field: engram.app.v1.McpApproval review = 6; + */ + review?: McpApproval; + + /** + * @generated from field: repeated string allowed_actions = 7; + */ + allowedActions: string[]; + + /** + * @generated from field: string reason = 8; + */ + reason: string; + + /** + * @generated from field: string tool_call_id = 9; + */ + toolCallId: string; + + /** + * @generated from field: string run_id = 10; + */ + runId: string; + + /** + * @generated from field: string request_digest = 11; + */ + requestDigest: string; + + /** + * @generated from field: string upstream_name = 12; + */ + upstreamName: string; + + /** + * @generated from field: string approval_decision = 13; + */ + approvalDecision: string; + + /** + * @generated from field: string original_arguments = 14; + */ + originalArguments: string; + + /** + * @generated from field: engram.app.v1.McpTool reviewed_tool = 15; + */ + reviewedTool?: McpTool; + + /** + * @generated from field: repeated string argument_constraint_documents_json = 16; + */ + argumentConstraintDocumentsJson: string[]; + + /** + * @generated from field: string binding_hash = 17; + */ + bindingHash: string; +}; + +/** + * Describes the message engram.app.v1.McpExecutionReview. + * Use `create(McpExecutionReviewSchema)` to create a new message. + */ +export const McpExecutionReviewSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 74); + +/** + * @generated from message engram.app.v1.McpResolutionReceipt + */ +export type McpResolutionReceipt = Message<"engram.app.v1.McpResolutionReceipt"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string actor_id = 2; + */ + actorId: string; + + /** + * @generated from field: string outcome = 3; + */ + outcome: string; + + /** + * @generated from field: string created_at = 4; + */ + createdAt: string; +}; + +/** + * Describes the message engram.app.v1.McpResolutionReceipt. + * Use `create(McpResolutionReceiptSchema)` to create a new message. + */ +export const McpResolutionReceiptSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 75); + +/** + * @generated from message engram.app.v1.McpBindingReceipt + */ +export type McpBindingReceipt = Message<"engram.app.v1.McpBindingReceipt"> & { + /** + * @generated from field: string binding_hash = 1; + */ + bindingHash: string; + + /** + * @generated from field: string binding_epoch = 2; + */ + bindingEpoch: string; + + /** + * @generated from field: bool current = 3; + */ + current: boolean; + + /** + * @generated from field: repeated string stale_reasons = 4; + */ + staleReasons: string[]; + + /** + * @generated from field: repeated string allowed_actions = 5; + */ + allowedActions: string[]; +}; + +/** + * Describes the message engram.app.v1.McpBindingReceipt. + * Use `create(McpBindingReceiptSchema)` to create a new message. + */ +export const McpBindingReceiptSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 76); + +/** + * @generated from message engram.app.v1.GetSessionMcpStateRequest + */ +export type GetSessionMcpStateRequest = Message<"engram.app.v1.GetSessionMcpStateRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string after_execution_id = 2; + */ + afterExecutionId: string; + + /** + * @generated from field: uint32 page_size = 3; + */ + pageSize: number; + + /** + * @generated from field: optional string plan_tool_call_id = 4; + */ + planToolCallId?: string; + + /** + * @generated from field: string after_binding_tool_name = 5; + */ + afterBindingToolName: string; + + /** + * @generated from field: uint32 binding_page_size = 6; + */ + bindingPageSize: number; + + /** + * @generated from field: bool omit_bindings = 7; + */ + omitBindings: boolean; + + /** + * @generated from field: bool omit_executions = 8; + */ + omitExecutions: boolean; +}; + +/** + * Describes the message engram.app.v1.GetSessionMcpStateRequest. + * Use `create(GetSessionMcpStateRequestSchema)` to create a new message. + */ +export const GetSessionMcpStateRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 77); + +/** + * @generated from message engram.app.v1.GetSessionMcpStateResponse + */ +export type GetSessionMcpStateResponse = Message<"engram.app.v1.GetSessionMcpStateResponse"> & { + /** + * @generated from field: engram.app.v1.McpBindingReceipt binding_receipt = 1; + */ + bindingReceipt?: McpBindingReceipt; + + /** + * @generated from field: repeated engram.app.v1.McpExecutionReview executions = 2; + */ + executions: McpExecutionReview[]; + + /** + * @generated from field: repeated engram.app.v1.McpApproval approvals = 3; + */ + approvals: McpApproval[]; + + /** + * @generated from field: repeated string unknown_write_blocks = 4; + */ + unknownWriteBlocks: string[]; + + /** + * @generated from field: repeated engram.app.v1.McpResolutionReceipt resolution_receipts = 5; + */ + resolutionReceipts: McpResolutionReceipt[]; + + /** + * @generated from field: repeated string allowed_actions = 6; + */ + allowedActions: string[]; + + /** + * @generated from field: string availability = 7; + */ + availability: string; + + /** + * @generated from field: string next_execution_id = 8; + */ + nextExecutionId: string; + + /** + * @generated from field: engram.app.v1.GetMcpWritePolicyResponse write_policy = 9; + */ + writePolicy?: GetMcpWritePolicyResponse; + + /** + * @generated from field: repeated engram.app.v1.McpBoundToolSummary binding_tools = 10; + */ + bindingTools: McpBoundToolSummary[]; + + /** + * @generated from field: string next_binding_tool_name = 11; + */ + nextBindingToolName: string; + + /** + * @generated from field: bool has_more_binding_tools = 12; + */ + hasMoreBindingTools: boolean; + + /** + * @generated from field: bool has_more_executions = 13; + */ + hasMoreExecutions: boolean; +}; + +/** + * Describes the message engram.app.v1.GetSessionMcpStateResponse. + * Use `create(GetSessionMcpStateResponseSchema)` to create a new message. + */ +export const GetSessionMcpStateResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 78); + +/** + * Immutable session grant. Schemas remain independent documents with their own roots. + * + * @generated from message engram.app.v1.McpBoundToolSummary + */ +export type McpBoundToolSummary = Message<"engram.app.v1.McpBoundToolSummary"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: string connection_alias = 2; + */ + connectionAlias: string; + + /** + * @generated from field: string model_tool_name = 3; + */ + modelToolName: string; + + /** + * @generated from field: engram.app.v1.McpTool reviewed_tool = 4; + */ + reviewedTool?: McpTool; + + /** + * @generated from field: repeated string argument_constraint_documents_json = 5; + */ + argumentConstraintDocumentsJson: string[]; + + /** + * @generated from field: bool automation_write = 6; + */ + automationWrite: boolean; + + /** + * @generated from field: bool unrestricted = 7; + */ + unrestricted: boolean; + + /** + * @generated from field: string catalog_id = 8; + */ + catalogId: string; + + /** + * @generated from field: uint32 catalog_revision = 9; + */ + catalogRevision: number; + + /** + * @generated from field: uint32 config_revision = 10; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 11; + */ + authGeneration: number; +}; + +/** + * Describes the message engram.app.v1.McpBoundToolSummary. + * Use `create(McpBoundToolSummarySchema)` to create a new message. + */ +export const McpBoundToolSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 79); /** * ADR 0057 C3: orchestrator-native connector catalog CRUD (Plane B). Connectors @@ -2202,7 +2830,7 @@ export const IntegrationService: GenService<{ putMcpCredential: { methodKind: "unary"; input: typeof PutMcpCredentialRequestSchema; - output: typeof McpCredentialResponseSchema; + output: typeof PutMcpCredentialResponseSchema; }, /** * @generated from rpc engram.app.v1.IntegrationService.GetMcpSetup @@ -2218,7 +2846,7 @@ export const IntegrationService: GenService<{ completeMcpAuthorization: { methodKind: "unary"; input: typeof CompleteMcpAuthorizationRequestSchema; - output: typeof McpCredentialResponseSchema; + output: typeof CompleteMcpAuthorizationResponseSchema; }, /** * @generated from rpc engram.app.v1.IntegrationService.RecoverMcpCredential @@ -2226,7 +2854,7 @@ export const IntegrationService: GenService<{ recoverMcpCredential: { methodKind: "unary"; input: typeof RecoverMcpCredentialRequestSchema; - output: typeof McpCredentialResponseSchema; + output: typeof RecoverMcpCredentialResponseSchema; }, /** * @generated from rpc engram.app.v1.IntegrationService.AuthorizeMcp @@ -2244,6 +2872,14 @@ export const IntegrationService: GenService<{ input: typeof DisconnectMcpRequestSchema; output: typeof DisconnectMcpResponseSchema; }, + /** + * @generated from rpc engram.app.v1.IntegrationService.GetSessionMcpState + */ + getSessionMcpState: { + methodKind: "unary"; + input: typeof GetSessionMcpStateRequestSchema; + output: typeof GetSessionMcpStateResponseSchema; + }, /** * @generated from rpc engram.app.v1.IntegrationService.ListMcpApprovals */ diff --git a/orchestrator/src/mcp/contracts.ts b/orchestrator/src/mcp/contracts.ts index c81d5abdd..427694836 100644 --- a/orchestrator/src/mcp/contracts.ts +++ b/orchestrator/src/mcp/contracts.ts @@ -1,3 +1,7 @@ +import { sessionMcpState, type McpReviewProducers } from "./review-state.ts"; +import { toJson } from "@bufbuild/protobuf"; +import { McpApprovalReviewContextSchema } from "../gen/engram/app/v1/integration_pb.ts"; +import { policyReviewCurrent, type JournalReviewContext } from "./journal-review.ts"; import { credentialHandlers } from "./credential-rpc.ts"; import type { CredentialAdmin } from "./credential-admin.ts"; import { Code, ConnectError, type ServiceImpl } from "@connectrpc/connect"; @@ -18,6 +22,7 @@ export interface McpServices { enabled: boolean; repository: McpRepository; journal: McpJournal; + reviewProducers?: McpReviewProducers; sessionOwner?: (sessionId: string) => Promise; validateDestination?: (config: McpConfig) => Promise; validateSchemas?: (tools: readonly McpReviewedTool[]) => Promise; @@ -143,6 +148,7 @@ type Methods = | "publishMcp" | "authorizeMcp" | "disconnectMcp" + | "getSessionMcpState" | "listMcpApprovals" | "decideMcpApproval" | "cancelMcpCall" @@ -217,53 +223,106 @@ export function mcpHandlers(deps: { }; }); }, - async listMcpApprovals(req, ctx) { + async getSessionMcpState(req, ctx) { const actor = await requireUser(ctx, deps.getSession); const service = services(); - return mcpOperation(async () => ({ - approvals: ( - await service.journal.pending(req.sessionId, actor.id, actor.role === "admin", deps.now()) - ).map(({ call, approval }) => ({ - executionId: call.id, - sessionId: call.sessionId, - runId: call.runId, - toolCallId: call.toolCallId, - connectionId: call.connectionId, - upstreamName: call.upstreamName, - requestDigest: call.requestDigest, - argumentHash: call.argumentHash, - expiresAt: approval.expiresAt.toISOString(), - })), - })); + return mcpOperation(() => + sessionMcpState({ + journal: service.journal, + sessionId: req.sessionId, + actorId: actor.id, + now: deps.now(), + after: req.afterExecutionId || undefined, + limit: req.pageSize || undefined, + planToolCallId: req.planToolCallId, + producers: service.reviewProducers, + canCancel: Boolean(service.runtime?.cancel), + canRebind: Boolean(service.runtime?.rebind), + afterBindingToolName: req.afterBindingToolName || undefined, + bindingPageSize: req.bindingPageSize || undefined, + omitBindings: req.omitBindings, + omitExecutions: req.omitExecutions, + }), + ); }, - async decideMcpApproval(req, ctx) { + async listMcpApprovals(req, ctx) { const actor = await requireUser(ctx, deps.getSession); const service = services(); - if (actor.serviceAccount) - throw new ConnectError("a human approval is required", Code.PermissionDenied); - return mcpOperation(async () => ({ - state: ( - await service.journal.decide( - req.executionId, - actor.id, - actor.role === "admin", - req.approve, - deps.now(), - ) - ).state, - })); + // Use the same bounded read; a continuation is available from GetSessionMcpState. + return mcpOperation(async () => { + const state = await sessionMcpState({ + journal: service.journal, + sessionId: req.sessionId, + actorId: actor.id, + now: deps.now(), + after: req.afterExecutionId || undefined, + producers: service.reviewProducers, + canCancel: Boolean(service.runtime?.cancel), + omitBindings: true, + }); + return { approvals: state.approvals, nextExecutionId: state.nextExecutionId }; + }); }, - async cancelMcpCall(req, ctx) { + async decideMcpApproval(req, ctx) { const actor = await requireUser(ctx, deps.getSession); const service = services(); - const runtime = requireMcpHook(service.runtime); + if (!req.expectedReviewContext) + throw new ConnectError("exact review context is required", Code.InvalidArgument); + // Copy known fields only. Protobuf runtime metadata is not review identity. + const expected = toJson(McpApprovalReviewContextSchema, req.expectedReviewContext, { + alwaysEmitImplicit: true, + }) as JournalReviewContext; return mcpOperation(async () => { - const call = await service.journal.cancel( + await service.journal.readSession(expected.sessionId, actor.id, { limit: 1 }); + const producers = requireMcpHook(service.reviewProducers); + const current = await producers.bindings.readCurrent(expected.sessionId); + const policy = await requireMcpHook(producers.policy.readWritePolicy)({ + protocol: 1, + sessionId: expected.sessionId, + }); + if ( + current.bindingHash !== expected.bindingHash || + !policy.state?.policy?.ref || + policy.state.policy.ref.policyRevision?.toString() !== expected.policyRevision || + policy.state.policy.ref.bindingEpoch?.toString() !== expected.bindingEpoch + ) + throw new Error("stale review policy"); + const call = await service.journal.get(req.executionId); + if (!call?.requestContext || !policyReviewCurrent(call.requestContext, policy.state.policy)) + throw new Error("stale review policy lineage"); + if ( + req.approve && + (!policy.state.policy.current || + !policy.state.policy.initialized || + policy.state.policy.writeEligibility !== "write_eligible") + ) + throw new Error("current policy denies writes"); + const result = await service.journal.decide( req.executionId, actor.id, - actor.role === "admin", + req.approve, + expected, deps.now(), ); + return { + state: result.call.state, + receipt: { + executionId: result.call.id, + decision: result.approval.decision, + actorId: result.approval.actorId!, + decidedAt: result.approval.decidedAt!.toISOString(), + journalRevision: result.call.version, + reviewedContext: result.reviewedContext, + }, + }; + }); + }, + async cancelMcpCall(req, ctx) { + const actor = await requireUser(ctx, deps.getSession); + const service = services(); + const runtime = requireMcpHook(service.runtime); + return mcpOperation(async () => { + const call = await service.journal.cancel(req.executionId, actor.id, deps.now()); await runtime.cancel(call.id); return { state: call.state }; }); @@ -278,13 +337,7 @@ export function mcpHandlers(deps: { const outcome = req.outcome; return mcpOperation(async () => ({ outcome: ( - await service.journal.resolveUnknown( - req.executionId, - actor.id, - actor.role === "admin", - outcome, - deps.now(), - ) + await service.journal.resolveUnknown(req.executionId, actor.id, outcome, deps.now()) ).outcome, })); }, diff --git a/orchestrator/src/mcp/journal-review.ts b/orchestrator/src/mcp/journal-review.ts new file mode 100644 index 000000000..dfb6746c3 --- /dev/null +++ b/orchestrator/src/mcp/journal-review.ts @@ -0,0 +1,328 @@ +import { + create, + fromJson, + toJson, + type JsonValue, + type MessageInitShape, +} from "@bufbuild/protobuf"; +import { and, eq } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { + user, + task, + taskSession, + profile, + mcpExecution, + mcpApproval, + integrationConnection, + mcpCatalog, +} from "../db/schema.ts"; +import { isServiceAccountEmail } from "../rpc/api-key.ts"; +import { + McpWritePolicyRefSchema, + McpTrustedWritePolicySchema, +} from "../gen/engram/app/v1/session_pb.ts"; +import type { RequestPolicyContext } from "./session-policy.ts"; +import type { ResolvedBoundTool } from "./session-resolver.ts"; +import { restoreBindingSnapshot } from "./binding-store.ts"; +import { canonicalDigest, canonicalJson } from "./hash.ts"; +import { MCP_LIMITS } from "./limits.ts"; + +type Db = ReturnType; +export type JournalTx = Parameters[0]>[0]; +export type FrozenRequestContext = { + resolvedTool: ResolvedBoundTool; + requestPolicyContext: Omit & { + acceptedPolicyRef: JsonValue; + ancestorPolicyRefs: JsonValue[]; + }; +}; +export type JournalReviewContext = { + executionId: string; + sessionId: string; + runId: string; + toolCallId: string; + requestDigest: string; + argumentHash: string; + bindingHash: string; + connectionId: string; + catalogId: string; + catalogRevision: number; + configRevision: number; + authGeneration: number; + credentialIdentity: string; + upstreamName: string; + toolFingerprint: string; + contextDigest: string; + journalRevision: number; + expiresAt: string; + policyRevision: string; + bindingEpoch: string; +}; +export function freezeRequestContext( + resolvedTool: ResolvedBoundTool, + policy: RequestPolicyContext, +): FrozenRequestContext { + const result = { + resolvedTool: structuredClone(resolvedTool), + requestPolicyContext: { + ...policy, + acceptedPolicyRef: toJson(McpWritePolicyRefSchema, policy.acceptedPolicyRef), + ancestorPolicyRefs: policy.ancestorPolicyRefs.map((ref) => + toJson(McpWritePolicyRefSchema, ref), + ), + }, + }; + if (Buffer.byteLength(canonicalJson(result)) > 2 * MCP_LIMITS.manifestBytes) + throw new Error("request context exceeds limit"); + return JSON.parse(canonicalJson(result)); +} +export function requestPolicyContext(context: FrozenRequestContext): RequestPolicyContext { + const policy = context.requestPolicyContext; + return { + ...policy, + acceptedPolicyRef: fromJson(McpWritePolicyRefSchema, policy.acceptedPolicyRef), + ancestorPolicyRefs: policy.ancestorPolicyRefs.map((ref) => + fromJson(McpWritePolicyRefSchema, ref), + ), + }; +} +/** Compare the complete frozen mode-policy lineage; a leaf revision alone is not enough. */ +export function policyReviewCurrent( + context: FrozenRequestContext, + current: MessageInitShape, +): boolean { + const policy = create(McpTrustedWritePolicySchema, current); + return ( + Boolean(policy.ref) && + canonicalDigest(context.requestPolicyContext.acceptedPolicyRef) === + canonicalDigest(toJson(McpWritePolicyRefSchema, policy.ref!)) && + canonicalDigest(context.requestPolicyContext.ancestorPolicyRefs) === + canonicalDigest(policy.ancestorPolicyRefs.map((ref) => toJson(McpWritePolicyRefSchema, ref))) + ); +} +export function assertOriginalArguments(original: string, expectedHash: string) { + if ( + typeof original !== "string" || + Buffer.byteLength(original) > MCP_LIMITS.argumentBytes || + Buffer.from(original).toString("utf8") !== original + ) + throw new Error("invalid argument bytes"); + let parsed: unknown; + try { + parsed = JSON.parse(original); + } catch { + throw new Error("invalid argument JSON"); + } + if ( + !parsed || + typeof parsed !== "object" || + Array.isArray(parsed) || + canonicalDigest(parsed) !== expectedHash + ) + throw new Error("argument digest mismatch"); +} +/** Shared locks serialize authorization with owner, role, ban and session changes. */ +export async function lockHumanSession( + tx: JournalTx, + sessionId: string, + actorId: string, + includeAncestors = false, +) { + const [actor] = await tx.select().from(user).where(eq(user.id, actorId)).for("share"); + if (!actor || actor.banned || isServiceAccountEmail(actor.email)) + throw new Error("human session owner or administrator required"); + const chain: { sessionId: string; taskId: string; parentId: string | null }[] = []; + let id: string | null = sessionId; + const seen = new Set(); + while (id) { + if (seen.has(id) || chain.length >= 32) throw new Error("stale MCP review ancestry"); + seen.add(id); + const refs = await tx + .select({ taskId: taskSession.taskId, parentId: task.spawningSessionId }) + .from(taskSession) + .innerJoin(task, eq(task.id, taskSession.taskId)) + .where(eq(taskSession.sessionId, id)) + .limit(2); + if (refs.length !== 1) throw new Error("human session owner or administrator required"); + chain.push({ sessionId: id, ...refs[0]! }); + id = includeAncestors ? refs[0]!.parentId : null; + } + // Global order: user -> sorted tasks -> sorted sessions -> sorted profiles/connections -> execution. + const tasks = new Map(); + for (const taskId of [...new Set(chain.map((ref) => ref.taskId))].sort()) { + const [row] = await tx.select().from(task).where(eq(task.id, taskId)).for("share"); + if (!row) throw new Error("human session owner or administrator required"); + tasks.set(taskId, row); + } + const sessions = new Map(); + for (const ref of [...chain].sort((a, b) => a.sessionId.localeCompare(b.sessionId))) { + const [row] = await tx + .select() + .from(taskSession) + .where(and(eq(taskSession.sessionId, ref.sessionId), eq(taskSession.taskId, ref.taskId))) + .for("share"); + // Revalidate the discovered chain after all relevant writes are blocked. + if (!row || tasks.get(ref.taskId)?.spawningSessionId !== ref.parentId) + throw new Error("stale MCP review ancestry"); + sessions.set(ref.sessionId, row); + } + const owner = tasks.get(chain[0]!.taskId)!, + session = sessions.get(sessionId)!; + if (actor.role !== "admin" && owner.createdByUserId !== actorId) + throw new Error("human session owner or administrator required"); + return { + owner, + session, + actor, + ancestors: chain + .slice(1) + .map((ref) => ({ session: sessions.get(ref.sessionId)!, owner: tasks.get(ref.taskId)! })), + }; +} +export async function assertLocalReviewCurrent( + tx: JournalTx, + call: typeof mcpExecution.$inferSelect, + session: typeof taskSession.$inferSelect, + ancestors: { session: typeof taskSession.$inferSelect; owner: typeof task.$inferSelect }[], +) { + if (!call.requestContext || call.originalArguments === null || !session.mcpBindingEnvelope) + throw new Error("original request context unavailable"); + const stored = restoreBindingSnapshot( + session.mcpBindingEnvelope, + session.integrationConnections ?? [], + ); + const bound = call.requestContext.resolvedTool; + const policy = requestPolicyContext(call.requestContext); + if ( + stored.bindingHash !== call.bindingHash || + BigInt(stored.bindingEpoch) !== policy.acceptedPolicyRef.bindingEpoch || + canonicalDigest(stored.ancestorAuthorityRefs) !== + canonicalDigest(bound.ancestorAuthorityRefs) || + !stored.immutableBindings.some( + (binding) => canonicalDigest(binding) === canonicalDigest(bound.binding), + ) + ) + throw new Error("stale MCP review binding"); + const snapshots = [stored]; + const [leafTask] = await tx.select().from(task).where(eq(task.id, session.taskId)); + let parentId = leafTask?.spawningSessionId; + const seen = new Set([session.sessionId]); + if (stored.ancestorAuthorityRefs.length >= 32) throw new Error("stale MCP review ancestry"); + for (let i = 0; i < stored.ancestorAuthorityRefs.length; i++) { + const expected = stored.ancestorAuthorityRefs[i]!; + if (parentId !== expected.sessionId || seen.has(parentId)) + throw new Error("stale MCP review ancestry"); + seen.add(parentId); + const ancestor = ancestors[i]?.session, + ancestorTask = ancestors[i]?.owner; + if ( + !ancestor?.mcpBindingEnvelope || + !ancestorTask || + ancestor.sessionId !== parentId || + ["done", "failed", "cancelled"].includes(ancestorTask.status) + ) + throw new Error("stale MCP review ancestry"); + const snapshot = restoreBindingSnapshot( + ancestor.mcpBindingEnvelope, + ancestor.integrationConnections ?? [], + ); + if ( + snapshot.bindingHash !== expected.bindingHash || + snapshot.bindingEpoch !== expected.bindingEpoch || + canonicalDigest(snapshot.ancestorAuthorityRefs) !== + canonicalDigest(stored.ancestorAuthorityRefs.slice(i + 1)) + ) + throw new Error("stale MCP review ancestry"); + snapshots.push(snapshot); + parentId = ancestorTask.spawningSessionId; + } + if (parentId || ancestors.length !== stored.ancestorAuthorityRefs.length) + throw new Error("stale MCP review ancestry"); + const profileIds = [...new Set(snapshots.map((snapshot) => snapshot.profileId))].sort(); + for (const id of profileIds) { + const [currentProfile] = await tx.select().from(profile).where(eq(profile.id, id)).for("share"); + if ( + !currentProfile || + currentProfile.deletedAt || + snapshots + .filter((snapshot) => snapshot.profileId === id) + .some( + (snapshot) => + canonicalDigest( + (currentProfile.integrationGrants ?? []).filter((grant) => grant.mcp !== undefined), + ) !== snapshot.profileGrantsHash, + ) + ) + throw new Error("stale MCP review profile"); + } + const bindings = snapshots.flatMap((snapshot) => snapshot.immutableBindings); + for (const id of [...new Set(bindings.map((binding) => binding.connectionId))].sort()) { + const [live] = await tx + .select() + .from(integrationConnection) + .where(eq(integrationConnection.id, id)) + .for("share"); + for (const binding of bindings.filter((binding) => binding.connectionId === id)) { + const [catalog] = await tx + .select() + .from(mcpCatalog) + .where(eq(mcpCatalog.id, binding.catalogId)); + if ( + !live || + live.provider !== "mcp" || + !live.enabled || + live.revoked || + live.alias !== binding.alias || + canonicalDigest(live.config) !== canonicalDigest(binding.config) || + live.configRevision !== binding.configRevision || + live.authGeneration !== binding.authGeneration || + live.credentialIdentity !== binding.credentialIdentity || + live.catalogRevision !== binding.catalogRevision || + !catalog || + catalog.kind !== "published" || + catalog.connectionId !== id || + catalog.revision !== binding.catalogRevision || + catalog.configRevision !== binding.configRevision || + catalog.authGeneration !== binding.authGeneration || + catalog.credentialIdentity !== binding.credentialIdentity || + binding.tools.some( + (bound) => + !catalog.tools.some((tool) => canonicalDigest(tool) === canonicalDigest(bound.tool)), + ) + ) + throw new Error("stale MCP review connection"); + } + } +} +export function reviewContext( + call: typeof mcpExecution.$inferSelect, + approval: typeof mcpApproval.$inferSelect, +): JournalReviewContext { + if (!call.requestContext || call.originalArguments === null) + throw new Error("original request context unavailable"); + const { resolvedTool } = call.requestContext, + policy = requestPolicyContext(call.requestContext); + return { + executionId: call.id, + sessionId: call.sessionId, + runId: call.runId, + toolCallId: call.toolCallId, + requestDigest: call.requestDigest, + argumentHash: call.argumentHash, + bindingHash: call.bindingHash, + connectionId: call.connectionId, + catalogId: call.catalogId, + catalogRevision: resolvedTool.binding.catalogRevision, + configRevision: call.configRevision, + authGeneration: call.authGeneration, + credentialIdentity: resolvedTool.binding.credentialIdentity, + upstreamName: call.upstreamName, + toolFingerprint: resolvedTool.reviewedTool.fingerprint, + contextDigest: canonicalDigest(call.requestContext), + journalRevision: call.version, + expiresAt: approval.expiresAt.toISOString(), + policyRevision: policy.acceptedPolicyRef.policyRevision.toString(), + bindingEpoch: policy.acceptedPolicyRef.bindingEpoch.toString(), + }; +} diff --git a/orchestrator/src/mcp/journal.ts b/orchestrator/src/mcp/journal.ts index c7bac4d08..3d30c38d7 100644 --- a/orchestrator/src/mcp/journal.ts +++ b/orchestrator/src/mcp/journal.ts @@ -8,9 +8,20 @@ import { mcpResolution as resolution, mcpAdmission as admission, mcpAdmissionScope as scope, - task, + taskSession, } from "../db/schema.ts"; -import { assertOwnerOrAdmin } from "./approvals.ts"; +import { + assertOriginalArguments, + freezeRequestContext, + lockHumanSession, + assertLocalReviewCurrent, + reviewContext, + requestPolicyContext, + type JournalReviewContext, +} from "./journal-review.ts"; +import type { RequestPolicyContext } from "./session-policy.ts"; +import type { ResolvedBoundTool } from "./session-resolver.ts"; +import { restoreBindingSnapshot } from "./binding-store.ts"; import { canonicalDigest, canonicalJson } from "./hash.ts"; import { MCP_LIMITS } from "./limits.ts"; import { validateMcpGrant, type McpToolGrant, type McpExecutionState } from "./types.ts"; @@ -37,24 +48,16 @@ export type McpCallInput = Pick< | "authGeneration" | "classification" | "deadline" ->; +> & { + originalArguments: string; + resolvedTool: ResolvedBoundTool; + requestPolicyContext: RequestPolicyContext; +}; async function lockScope(tx: Tx, id: string) { await tx.insert(scope).values({ id }).onConflictDoNothing(); await tx.select().from(scope).where(eq(scope.id, id)).for("update"); } -async function owned( - tx: Tx, - call: typeof execution.$inferSelect, - actorId: string, - isAdmin: boolean, -) { - const [owner] = await tx - .select({ id: task.createdByUserId }) - .from(task) - .where(eq(task.id, call.taskId)); - assertOwnerOrAdmin(owner?.id ?? null, actorId, isAdmin); -} async function unknown(tx: Tx | Db, sessionId: string, connectionId: string): Promise { const rows = await tx .select({ id: execution.id }) @@ -73,17 +76,63 @@ async function unknown(tx: Tx | Db, sessionId: string, connectionId: string): Pr return rows.length > 0; } +/** Internal runtime proof; no public or guest route accepts this value. The runtime obtains it + * from its fresh principal, full binding/ancestry, schema, policy and kill checks. */ +export interface McpLiveDispatchAuthorization { + executionId: string; + requestDigest: string; + bindingHash: string; + principalId: string; + expectedJournalRevision: number; + reservationId: string; + reservationExpiresAt: Date; +} export function makeMcpJournal(db = getDb()) { return { async get(id: string) { return (await db.select().from(execution).where(eq(execution.id, id)))[0] ?? null; }, + async getAdmission(id: string) { + return (await db.select().from(admission).where(eq(admission.executionId, id)))[0] ?? null; + }, hasUnknown(sessionId: string, connectionId: string) { return unknown(db, sessionId, connectionId); }, async create(input: McpCallInput, authorization: McpCallAuthorization, now: Date) { input = structuredClone(input); authorization = structuredClone(authorization); + assertOriginalArguments(input.originalArguments, input.argumentHash); + const context = freezeRequestContext(input.resolvedTool, input.requestPolicyContext); + const { resolvedTool, requestPolicyContext: policy, ...storedInput } = input; + const bound = resolvedTool.binding; + if ( + policy.sessionId !== input.sessionId || + policy.runId !== input.runId || + policy.toolCallId !== input.toolCallId || + policy.acceptedPolicyRef.sessionId !== input.sessionId || + policy.acceptedPolicyRef.bindingEpoch < 0n || + policy.acceptedPolicyRef.bindingEpoch > BigInt(Number.MAX_SAFE_INTEGER) || + policy.ancestorPolicyRefs.length !== resolvedTool.ancestorAuthorityRefs.length || + policy.ancestorPolicyRefs.some( + (ref, index) => + ref.sessionId !== resolvedTool.ancestorAuthorityRefs[index]?.sessionId || + ref.bindingEpoch !== BigInt(resolvedTool.ancestorAuthorityRefs[index]!.bindingEpoch), + ) || + resolvedTool.bindingHash !== input.bindingHash || + bound.connectionId !== input.connectionId || + bound.catalogId !== input.catalogId || + bound.configRevision !== input.configRevision || + bound.authGeneration !== input.authGeneration || + resolvedTool.reviewedTool.upstreamName !== input.upstreamName || + resolvedTool.reviewedTool.classification !== input.classification || + !bound.tools.some( + (tool) => + tool.name === resolvedTool.modelToolName && + canonicalDigest(tool.tool) === canonicalDigest(resolvedTool.reviewedTool) && + canonicalDigest(tool.grant) === canonicalDigest(resolvedTool.effectiveGrant), + ) + ) + throw new Error("request review identity mismatch"); if ( ![input.argumentHash, input.bindingHash].every((hash) => /^sha256:[0-9a-f]{64}$/.test(hash)) ) @@ -110,7 +159,8 @@ export function makeMcpJournal(db = getDb()) { if ( input.classification !== "write" || !authorization.grant.automationWrite || - authorization.grant.upstreamName !== input.upstreamName + authorization.grant.upstreamName !== input.upstreamName || + canonicalDigest(authorization.grant) !== canonicalDigest(resolvedTool.effectiveGrant) ) throw new Error("explicit automation write grant is required"); } @@ -119,16 +169,40 @@ export function makeMcpJournal(db = getDb()) { const automationGrantHash = authorization.kind === "automation" ? canonicalDigest(authorization.grant) : null; const requestDigest = canonicalDigest({ - ...input, + ...storedInput, + requestContext: context, deadline: input.deadline.toISOString(), authorizationKind, automationGrantHash, }); return db.transaction(async (tx) => { + const [session] = await tx + .select() + .from(taskSession) + .where( + and(eq(taskSession.sessionId, input.sessionId), eq(taskSession.taskId, input.taskId)), + ) + .for("share"); + if (!session?.mcpBindingEnvelope) throw new Error("stored binding unavailable"); + const snapshot = restoreBindingSnapshot( + session.mcpBindingEnvelope, + session.integrationConnections ?? [], + ); + if ( + snapshot.bindingHash !== input.bindingHash || + BigInt(snapshot.bindingEpoch) !== policy.acceptedPolicyRef.bindingEpoch || + !snapshot.immutableBindings.some( + (binding) => canonicalDigest(binding) === canonicalDigest(bound), + ) || + canonicalDigest(snapshot.ancestorAuthorityRefs) !== + canonicalDigest(resolvedTool.ancestorAuthorityRefs) + ) + throw new Error("stale request binding"); await tx .insert(execution) .values({ - ...input, + ...storedInput, + requestContext: context, id: crypto.randomUUID(), state, authorizationKind, @@ -191,8 +265,9 @@ export function makeMcpJournal(db = getDb()) { return row!; }); }, - async pending(sessionId: string, actorId: string, isAdmin: boolean, now: Date) { + async pending(sessionId: string, actorId: string, now: Date) { return db.transaction(async (tx) => { + await lockHumanSession(tx, sessionId, actorId); const rows = await tx .select({ call: execution, approval }) .from(execution) @@ -203,30 +278,136 @@ export function makeMcpJournal(db = getDb()) { eq(approval.decision, "pending"), gt(approval.expiresAt, now), ), - ); - for (const row of rows) await owned(tx, row.call, actorId, isAdmin); - return rows; + ) + .orderBy(asc(execution.id)) + .limit(MCP_LIMITS.sessionApprovals); + return rows.filter((row) => row.call.requestContext && row.call.originalArguments !== null); }); }, - async decide(id: string, actorId: string, isAdmin: boolean, approve: boolean, now: Date) { + async readSession( + sessionId: string, + actorId: string, + page: { after?: string; limit?: number } = {}, + ) { + const limit = page.limit ?? 16; + if ( + !Number.isSafeInteger(limit) || + limit < 1 || + limit > 16 || + (page.after?.length ?? 0) > 128 + ) + throw new Error("invalid state page"); return db.transaction(async (tx) => { - const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); - if (!call) throw new Error("call not found"); - await owned(tx, call, actorId, isAdmin); - if (call.state !== "waiting_approval") throw new Error("approval is no longer pending"); + const { session } = await lockHumanSession(tx, sessionId, actorId); const rows = await tx - .update(approval) - .set({ decision: approve ? "approved" : "denied", actorId, decidedAt: now }) + .select({ call: execution, approval, resolution }) + .from(execution) + .leftJoin(approval, eq(approval.executionId, execution.id)) + .leftJoin(resolution, eq(resolution.executionId, execution.id)) .where( and( - eq(approval.executionId, id), - eq(approval.requestDigest, call.requestDigest), - eq(approval.decision, "pending"), - gt(approval.expiresAt, now), + eq(execution.sessionId, sessionId), + page.after ? gt(execution.id, page.after) : undefined, + ), + ) + .orderBy(asc(execution.id)) + .limit(limit + 1); + const blocks = await tx + .selectDistinct({ connectionId: execution.connectionId }) + .from(execution) + .leftJoin(resolution, eq(resolution.executionId, execution.id)) + .where( + and( + eq(execution.sessionId, sessionId), + eq(execution.classification, "write"), + eq(execution.state, "outcome_unknown"), + isNull(resolution.executionId), ), ) + .limit(17); + if (blocks.length > 16) throw new Error("unknown connection state exceeds limit"); + return { + session, + rows: rows.slice(0, limit), + hasMore: rows.length > limit, + unknownWriteBlocks: blocks.map((row) => row.connectionId), + }; + }); + }, + async decide( + id: string, + actorId: string, + approve: boolean, + expected: JournalReviewContext, + now: Date, + ) { + return db.transaction(async (tx) => { + const [identity] = await tx.select().from(execution).where(eq(execution.id, id)); + if (!identity) throw new Error("call not found"); + const { session, owner, ancestors } = await lockHumanSession( + tx, + identity.sessionId, + actorId, + true, + ); + // Lock profile before connection/execution; no network call is made in this transaction. + await assertLocalReviewCurrent(tx, identity, session, ancestors); + const [live] = await tx + .select() + .from(connection) + .where(eq(connection.id, identity.connectionId)) + .for("share"); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + const [item] = await tx + .select() + .from(approval) + .where(eq(approval.executionId, id)) + .for("update"); + if ( + !call || + call.requestDigest !== identity.requestDigest || + call.taskId !== owner.id || + call.state !== "waiting_approval" || + !item || + item.decision !== "pending" || + item.expiresAt <= now || + call.deadline <= now + ) + throw new Error("approval expired or already decided"); + if (!expected || canonicalDigest(expected) !== canonicalDigest(reviewContext(call, item))) + throw new Error("stale review context"); + if ( + approve && + call.classification === "write" && + requestPolicyContext(call.requestContext!).acceptedWriteEligibility !== "write_eligible" + ) + throw new Error("request policy denies writes"); + const frozen = call.requestContext!.resolvedTool; + const [review] = await tx.select().from(catalog).where(eq(catalog.id, call.catalogId)); + if ( + ["done", "failed", "cancelled"].includes(owner.status) || + !live?.enabled || + live.revoked || + live.alias !== frozen.binding.alias || + canonicalDigest(live.config) !== canonicalDigest(frozen.binding.config) || + live.configRevision !== call.configRevision || + live.authGeneration !== call.authGeneration || + live.credentialIdentity !== frozen.binding.credentialIdentity || + live.catalogRevision !== frozen.binding.catalogRevision || + !review || + review.kind !== "published" || + review.connectionId !== call.connectionId || + review.revision !== frozen.binding.catalogRevision || + !review.tools.some( + (tool) => canonicalDigest(tool) === canonicalDigest(frozen.reviewedTool), + ) + ) + throw new Error("stale MCP review authority"); + const [decision] = await tx + .update(approval) + .set({ decision: approve ? "approved" : "denied", actorId, decidedAt: now }) + .where(eq(approval.executionId, id)) .returning(); - if (!rows.length) throw new Error("approval expired or already decided"); const [updated] = await tx .update(execution) .set({ @@ -236,7 +417,7 @@ export function makeMcpJournal(db = getDb()) { }) .where(eq(execution.id, id)) .returning(); - return updated!; + return { call: updated!, approval: decision!, reviewedContext: expected }; }); }, async admit( @@ -303,104 +484,161 @@ export function makeMcpJournal(db = getDb()) { id: string, intent: { bootId: string; intentId: string; expiresAt: Date }, now: Date, + liveAuthorization: McpLiveDispatchAuthorization, ) { - return db.transaction(async (tx) => { - // Connection lock serializes unknown-outcome checks with finish/revoke. - const [identity] = await tx - .select({ connectionId: execution.connectionId }) - .from(execution) - .where(eq(execution.id, id)); - if (!identity) throw new Error("call not found"); - const [live] = await tx - .select() - .from(connection) - .where(eq(connection.id, identity.connectionId)) - .for("update"); - const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); - if (!call || call.state !== "authorized" || call.deadline <= now) - throw new Error("call cannot dispatch"); - const [review] = await tx.select().from(catalog).where(eq(catalog.id, call.catalogId)); - if ( - !live || - live.provider !== "mcp" || - !live.enabled || - live.revoked || - live.configRevision !== call.configRevision || - live.authGeneration !== call.authGeneration || - !review || - review.connectionId !== live.id || - review.kind !== "published" || - review.revision !== live.catalogRevision || - review.configRevision !== call.configRevision || - review.authGeneration !== call.authGeneration || - review.credentialIdentity !== live.credentialIdentity - ) - throw new Error("stale MCP authority"); - const reviewedTool = review.tools.find((t) => t.upstreamName === call.upstreamName); - if ( - !reviewedTool || - !("classification" in reviewedTool) || - reviewedTool.classification !== call.classification || - !("enabled" in reviewedTool) || - !reviewedTool.enabled - ) - throw new Error("tool is not approved for this operation"); - if ( - call.classification === "write" && - (await unknown(tx, call.sessionId, call.connectionId)) - ) - throw new Error("unknown write outcome blocks this connection"); - const [lease] = await tx - .select() - .from(admission) - .where( - and( - eq(admission.executionId, id), - eq(admission.state, "admitted"), - gt(admission.expiresAt, now), - ), - ); - if ( - !lease || - !intent.bootId || - !intent.intentId || - intent.expiresAt <= now || - intent.expiresAt > lease.expiresAt - ) - throw new Error("valid admission and intent are required"); - const [decision] = await tx.select().from(approval).where(eq(approval.executionId, id)); - if (call.authorizationKind === "approval" && !decision) - throw new Error("approval is required"); - if (decision) { - const consumed = await tx - .update(approval) - .set({ consumedAt: now }) + liveAuthorization = structuredClone(liveAuthorization); + intent = structuredClone(intent); + const [reservation] = await db.select().from(admission).where(eq(admission.executionId, id)); + if (!reservation) throw new Error("valid admission is required"); + try { + return await db.transaction(async (tx) => { + await lockScope(tx, reservation.deploymentId); + // Connection lock serializes unknown-outcome checks with finish/revoke. + const [identity] = await tx + .select({ connectionId: execution.connectionId }) + .from(execution) + .where(eq(execution.id, id)); + if (!identity) throw new Error("call not found"); + const [live] = await tx + .select() + .from(connection) + .where(eq(connection.id, identity.connectionId)) + .for("update"); + const [call] = await tx + .select() + .from(execution) + .where(eq(execution.id, id)) + .for("update"); + if (!call || call.state !== "authorized" || call.deadline <= now) + throw new Error("call cannot dispatch"); + if ( + !call.requestContext || + call.originalArguments === null || + !liveAuthorization || + liveAuthorization.executionId !== id || + liveAuthorization.requestDigest !== call.requestDigest || + liveAuthorization.bindingHash !== call.bindingHash || + liveAuthorization.principalId !== call.principalId || + liveAuthorization.expectedJournalRevision !== call.version + ) + throw new Error("stale dispatch authorization"); + const frozenPolicy = requestPolicyContext(call.requestContext); + if ( + call.classification === "write" && + frozenPolicy.acceptedWriteEligibility !== "write_eligible" + ) + throw new Error("request policy denies writes"); + const [review] = await tx.select().from(catalog).where(eq(catalog.id, call.catalogId)); + if ( + !live || + live.provider !== "mcp" || + !live.enabled || + live.revoked || + live.configRevision !== call.configRevision || + live.authGeneration !== call.authGeneration || + !review || + review.connectionId !== live.id || + review.kind !== "published" || + review.revision !== live.catalogRevision || + review.configRevision !== call.configRevision || + review.authGeneration !== call.authGeneration || + review.credentialIdentity !== live.credentialIdentity + ) + throw new Error("stale MCP authority"); + const reviewedTool = review.tools.find((t) => t.upstreamName === call.upstreamName); + if ( + !reviewedTool || + !("classification" in reviewedTool) || + reviewedTool.classification !== call.classification || + !("enabled" in reviewedTool) || + !reviewedTool.enabled + ) + throw new Error("tool is not approved for this operation"); + if ( + call.classification === "write" && + (await unknown(tx, call.sessionId, call.connectionId)) + ) + throw new Error("unknown write outcome blocks this connection"); + const [lease] = await tx + .select() + .from(admission) .where( and( - eq(approval.executionId, id), - eq(approval.requestDigest, call.requestDigest), - eq(approval.decision, "approved"), - isNull(approval.consumedAt), - gt(approval.expiresAt, now), + eq(admission.executionId, id), + eq(admission.state, "admitted"), + gt(admission.expiresAt, now), ), - ) + ); + if ( + !lease || + lease.reservationId !== reservation.reservationId || + lease.reservationId !== liveAuthorization.reservationId || + lease.expiresAt.getTime() !== liveAuthorization.reservationExpiresAt.getTime() || + !intent.bootId || + !intent.intentId || + intent.expiresAt <= now || + intent.expiresAt > lease.expiresAt + ) + throw new Error("valid admission and intent are required"); + const [decision] = await tx.select().from(approval).where(eq(approval.executionId, id)); + if (call.authorizationKind === "approval" && !decision) + throw new Error("approval is required"); + if (decision) { + const consumed = await tx + .update(approval) + .set({ consumedAt: now }) + .where( + and( + eq(approval.executionId, id), + eq(approval.requestDigest, call.requestDigest), + eq(approval.decision, "approved"), + isNull(approval.consumedAt), + gt(approval.expiresAt, now), + ), + ) + .returning(); + if (!consumed.length) throw new Error("approval expired or consumed"); + } + const [updated] = await tx + .update(execution) + .set({ + state: "dispatching", + version: call.version + 1, + bootId: intent.bootId, + intentId: intent.intentId, + intentExpiresAt: intent.expiresAt, + updatedAt: now, + }) + .where(eq(execution.id, id)) .returning(); - if (!consumed.length) throw new Error("approval expired or consumed"); - } - const [updated] = await tx - .update(execution) - .set({ - state: "dispatching", - version: call.version + 1, - bootId: intent.bootId, - intentId: intent.intentId, - intentExpiresAt: intent.expiresAt, - updatedAt: now, - }) - .where(eq(execution.id, id)) - .returning(); - return updated!; - }); + return updated!; + }); + } catch (error) { + // Release only a pre-barrier reservation. A concurrent committed dispatch keeps its lease. + await db.transaction(async (tx) => { + await lockScope(tx, reservation.deploymentId); + const [call] = await tx + .select() + .from(execution) + .where(eq(execution.id, id)) + .for("update"); + if ( + call?.state === "authorized" && + reservation.reservationId === liveAuthorization?.reservationId && + reservation.expiresAt.getTime() === liveAuthorization.reservationExpiresAt.getTime() + ) + await tx + .delete(admission) + .where( + and( + eq(admission.executionId, id), + eq(admission.reservationId, reservation.reservationId), + eq(admission.expiresAt, reservation.expiresAt), + ), + ); + }); + throw error; + } }, async finish( id: string, @@ -445,20 +683,21 @@ export function makeMcpJournal(db = getDb()) { return row; }); }, - async cancel(id: string, actorId: string, isAdmin: boolean, now: Date) { + async cancel(id: string, actorId: string, now: Date) { return db.transaction(async (tx) => { const [identity] = await tx - .select({ connectionId: execution.connectionId }) + .select({ connectionId: execution.connectionId, sessionId: execution.sessionId }) .from(execution) .where(eq(execution.id, id)); if (!identity) throw new Error("call not found"); + await lockHumanSession(tx, identity.sessionId, actorId); await tx .select() .from(connection) .where(eq(connection.id, identity.connectionId)) .for("update"); const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); - await owned(tx, call!, actorId, isAdmin); + if (!["waiting_approval", "authorized", "dispatching"].includes(call!.state)) return call!; const state: McpExecutionState = call!.state === "dispatching" ? "outcome_unknown" : "canceled"; @@ -478,15 +717,22 @@ export function makeMcpJournal(db = getDb()) { async resolveUnknown( id: string, actorId: string, - isAdmin: boolean, outcome: "succeeded" | "failed" | "acknowledged", now: Date, ) { return db.transaction(async (tx) => { + const [identity] = await tx.select().from(execution).where(eq(execution.id, id)); + if (!identity) throw new Error("call not found"); + await lockHumanSession(tx, identity.sessionId, actorId); + await tx + .select() + .from(connection) + .where(eq(connection.id, identity.connectionId)) + .for("update"); const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); if (!call || call.state !== "outcome_unknown") throw new Error("call has no unknown outcome"); - await owned(tx, call, actorId, isAdmin); + const [row] = await tx .insert(resolution) .values({ executionId: id, actorId, outcome, createdAt: now }) diff --git a/orchestrator/src/mcp/review-state.ts b/orchestrator/src/mcp/review-state.ts new file mode 100644 index 000000000..e7a618048 --- /dev/null +++ b/orchestrator/src/mcp/review-state.ts @@ -0,0 +1,279 @@ +import { create, toBinary, toJson, type MessageInitShape } from "@bufbuild/protobuf"; +import { GetSessionMcpStateResponseSchema } from "../gen/engram/app/v1/integration_pb.ts"; +import type { McpJournal } from "./journal.ts"; +import { reviewContext, policyReviewCurrent } from "./journal-review.ts"; +import { restoreBindingSnapshot } from "./binding-store.ts"; +import type { SessionBindingResolver } from "./session-resolver.ts"; +import type { SessionPolicyControl } from "./session-policy.ts"; + +function stateSize(value: MessageInitShape): number { + const message = create(GetSessionMcpStateResponseSchema, value); + return Math.max( + toBinary(GetSessionMcpStateResponseSchema, message).byteLength, + Buffer.byteLength(JSON.stringify(toJson(GetSessionMcpStateResponseSchema, message))), + ); +} + +type SessionRows = Awaited>; +export interface McpReviewProducers { + bindings: Pick; + policy: SessionPolicyControl; +} +export function approvalToProto(row: { + call: SessionRows["rows"][number]["call"]; + approval: NonNullable; +}) { + const { call, approval } = row; + if (!call.requestContext || call.originalArguments === null) + throw new Error("original request context unavailable"); + const tool = call.requestContext.resolvedTool.reviewedTool; + return { + executionId: call.id, + sessionId: call.sessionId, + runId: call.runId, + toolCallId: call.toolCallId, + connectionId: call.connectionId, + upstreamName: call.upstreamName, + requestDigest: call.requestDigest, + argumentHash: call.argumentHash, + expiresAt: approval.expiresAt.toISOString(), + originalArguments: call.originalArguments, + reviewedTool: { + ...tool, + inputSchemaJson: JSON.stringify(tool.inputSchema), + outputSchemaJson: tool.outputSchema ? JSON.stringify(tool.outputSchema) : "", + }, + argumentConstraintDocumentsJson: + call.requestContext.resolvedTool.effectiveGrant.argumentConstraintDocuments.map((document) => + JSON.stringify(document), + ), + expectedReviewContext: reviewContext(call, approval), + }; +} +/** Compose external freshness after the protected database read. No network inside PG locks. */ +export async function sessionMcpState(input: { + journal: McpJournal; + sessionId: string; + actorId: string; + now: Date; + after?: string; + limit?: number; + planToolCallId?: string; + producers?: McpReviewProducers; + canCancel?: boolean; + canRebind?: boolean; + afterBindingToolName?: string; + bindingPageSize?: number; + omitBindings?: boolean; + omitExecutions?: boolean; +}) { + const bindingLimit = input.bindingPageSize ?? 16; + if ( + !Number.isSafeInteger(bindingLimit) || + bindingLimit < 1 || + bindingLimit > 16 || + (input.afterBindingToolName?.length ?? 0) > 128 + ) + throw new Error("invalid binding page"); + const data = await input.journal.readSession(input.sessionId, input.actorId, { + after: input.after, + limit: input.limit, + }); + const response: MessageInitShape = { + executions: [], + bindingTools: [], + approvals: [], + unknownWriteBlocks: data.unknownWriteBlocks, + resolutionReceipts: [], + allowedActions: [], + availability: "unavailable", + }; + let current = false; + const envelope = data.session.mcpBindingEnvelope; + let snapshot: ReturnType | undefined; + if (envelope) { + response.bindingReceipt = { + bindingHash: envelope.bindingHash, + bindingEpoch: String(envelope.bindingEpoch), + current: false, + staleReasons: ["live_authority_unavailable"], + allowedActions: [], + }; + try { + snapshot = restoreBindingSnapshot(envelope, data.session.integrationConnections ?? []); + if (!input.producers?.policy.readWritePolicy) throw new Error("live producers missing"); + const live = await input.producers.bindings.readCurrent(input.sessionId); + const policy = await input.producers.policy.readWritePolicy({ + protocol: 1, + sessionId: input.sessionId, + planToolCallId: input.planToolCallId, + }); + if ( + !policy.state?.policy?.ref || + policy.state.policy.ref.sessionId !== input.sessionId || + live.bindingHash !== envelope.bindingHash + ) + throw new Error("live authority changed"); + response.writePolicy = policy.state; + response.writePolicy.allowedActions = policy.state.planReview ? ["approve_current_plan"] : []; + current = true; + response.bindingReceipt.current = true; + response.bindingReceipt.staleReasons = []; + response.availability = "available"; + } catch { + response.bindingReceipt.staleReasons = ["binding_or_policy_unavailable"]; + } + } + if (input.canRebind && snapshot && response.bindingReceipt) + response.bindingReceipt.allowedActions = ["rebind"]; + let last = input.after ?? ""; + for (const row of input.omitExecutions ? [] : data.rows) { + const { call, approval, resolution } = row; + const actions: string[] = []; + const review = + approval && call.requestContext && call.originalArguments !== null + ? approvalToProto({ call, approval }) + : undefined; + let reason = call.requestContext ? "" : "original_request_unavailable"; + if ( + current && + review && + call.requestContext && + response.writePolicy?.policy && + policyReviewCurrent(call.requestContext, response.writePolicy.policy) && + approval?.decision === "pending" && + call.state === "waiting_approval" && + approval.expiresAt > input.now && + call.deadline > input.now && + response.writePolicy?.policy?.ref?.policyRevision?.toString() === + review.expectedReviewContext.policyRevision && + response.writePolicy.policy.ref.bindingEpoch?.toString() === + review.expectedReviewContext.bindingEpoch + ) { + actions.push("reject"); + if ( + call.requestContext?.requestPolicyContext.acceptedWriteEligibility === "write_eligible" && + response.writePolicy.policy.current && + response.writePolicy.policy.initialized && + response.writePolicy.policy.writeEligibility === "write_eligible" + ) + actions.push("approve"); + } else if (call.state === "waiting_approval") + reason = "review_expired_or_authority_unavailable"; + if (input.canCancel && ["waiting_approval", "authorized", "dispatching"].includes(call.state)) + actions.push("cancel"); + if (call.state === "outcome_unknown" && !resolution) actions.push("resolve_unknown"); + const frozenTool = call.requestContext?.resolvedTool.reviewedTool; + const content = + !review && frozenTool + ? { + originalArguments: call.originalArguments ?? "", + reviewedTool: { + ...frozenTool, + inputSchemaJson: JSON.stringify(frozenTool.inputSchema), + outputSchemaJson: frozenTool.outputSchema + ? JSON.stringify(frozenTool.outputSchema) + : "", + }, + argumentConstraintDocumentsJson: + call.requestContext!.resolvedTool.effectiveGrant.argumentConstraintDocuments.map( + (document) => JSON.stringify(document), + ), + } + : {}; + const entry = { + ...content, + bindingHash: call.bindingHash, + executionId: call.id, + connectionId: call.connectionId, + state: call.state, + journalRevision: call.version, + resultUnavailable: call.resultUnavailable, + review: actions.includes("approve") ? undefined : review, + allowedActions: actions, + reason, + toolCallId: call.toolCallId, + runId: call.runId, + requestDigest: call.requestDigest, + upstreamName: call.upstreamName, + approvalDecision: approval?.decision ?? "", + }; + const candidate = { + ...response, + executions: [...response.executions!, entry], + approvals: [ + ...response.approvals!, + ...(review && actions.includes("approve") ? [review] : []), + ], + resolutionReceipts: [ + ...response.resolutionReceipts!, + ...(resolution + ? [ + { + executionId: resolution.executionId, + actorId: resolution.actorId, + outcome: resolution.outcome, + createdAt: resolution.createdAt.toISOString(), + }, + ] + : []), + ], + }; + if (stateSize(candidate) > 2 * 1024 * 1024 - 256) { + if (last === (input.after ?? "")) throw new Error("review exceeds state page limit"); + response.nextExecutionId = last; + response.hasMoreExecutions = true; + break; + } + Object.assign(response, candidate); + last = call.id; + } + if (!input.omitExecutions && data.hasMore) { + response.nextExecutionId = last; + response.hasMoreExecutions = true; + } + if (snapshot && !input.omitBindings) { + const tools = snapshot.immutableBindings + .flatMap((binding) => binding.tools.map((bound) => ({ binding, bound }))) + .sort((a, b) => (a.bound.name < b.bound.name ? -1 : a.bound.name > b.bound.name ? 1 : 0)) + .filter( + (item) => !input.afterBindingToolName || item.bound.name > input.afterBindingToolName, + ); + let lastTool = input.afterBindingToolName ?? "", + added = 0; + for (const { binding, bound } of tools) { + const summary = { + connectionId: binding.connectionId, + connectionAlias: binding.alias, + modelToolName: bound.name, + reviewedTool: { + ...bound.tool, + inputSchemaJson: JSON.stringify(bound.tool.inputSchema), + outputSchemaJson: bound.tool.outputSchema ? JSON.stringify(bound.tool.outputSchema) : "", + }, + argumentConstraintDocumentsJson: bound.grant.argumentConstraintDocuments.map((document) => + JSON.stringify(document), + ), + automationWrite: bound.grant.automationWrite, + unrestricted: bound.grant.unrestricted, + catalogId: binding.catalogId, + catalogRevision: binding.catalogRevision, + configRevision: binding.configRevision, + authGeneration: binding.authGeneration, + }; + const candidate = { ...response, bindingTools: [...response.bindingTools!, summary] }; + if (added === bindingLimit || stateSize(candidate) > 2 * 1024 * 1024 - 256) { + if (input.omitExecutions && added === 0) + throw new Error("binding review exceeds page limit"); + response.hasMoreBindingTools = true; + response.nextBindingToolName = lastTool; + break; + } + Object.assign(response, candidate); + lastTool = bound.name; + added++; + } + } + if (stateSize(response) > 2 * 1024 * 1024) throw new Error("state exceeds limit"); + return response; +} diff --git a/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts b/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts index 7eb5ab744..ee8803835 100644 --- a/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts +++ b/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts @@ -156,6 +156,11 @@ export const authorizeMcp = IntegrationService.method.authorizeMcp; */ export const disconnectMcp = IntegrationService.method.disconnectMcp; +/** + * @generated from rpc engram.app.v1.IntegrationService.GetSessionMcpState + */ +export const getSessionMcpState = IntegrationService.method.getSessionMcpState; + /** * @generated from rpc engram.app.v1.IntegrationService.ListMcpApprovals */ diff --git a/web/src/gen/engram/app/v1/integration_pb.ts b/web/src/gen/engram/app/v1/integration_pb.ts index 5d5c0784f..d0c59bd2b 100644 --- a/web/src/gen/engram/app/v1/integration_pb.ts +++ b/web/src/gen/engram/app/v1/integration_pb.ts @@ -4,13 +4,15 @@ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GetMcpWritePolicyResponse } from "./session_pb"; +import { file_engram_app_v1_session } from "./session_pb"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file engram/app/v1/integration.proto. */ export const file_engram_app_v1_integration: GenFile = /*@__PURE__*/ - fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIs4BCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkiLQoXTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCSJJChhMaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USLQoJYXBwcm92YWxzGAEgAygLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbCJBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgiKgoZRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRINCgVzdGF0ZRgBIAEoCSIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAkilQEKF1B1dE1jcENyZWRlbnRpYWxSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAiABKA0SGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgDIAEoDRIMCgRraW5kGAQgASgJEg4KBmhlYWRlchgFIAEoCRINCgV2YWx1ZRgGIAEoCSKUAQoTTWNwQ3JlZGVudGlhbFN0YXR1cxIOCgZzdGF0dXMYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJEgwKBGtpbmQYAyABKAkSGwoTY3JlZGVudGlhbF9pZGVudGl0eRgEIAEoCRIPCgd2ZXJzaW9uGAUgASgDEh4KFnJlc2VydmF0aW9uX2dlbmVyYXRpb24YBiABKAMiiQEKFU1jcENyZWRlbnRpYWxSZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyI7ChZNY3BBcHByb3ZlZERlc3RpbmF0aW9uEg8KB3pvbmVfaWQYASABKAkSEAoIZW5kcG9pbnQYAiABKAkiuwEKFU1jcENsaWVudFJlZ2lzdHJhdGlvbhIKCgJpZBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSFAoMY29uZmlkZW50aWFsGAMgASgIEhQKDHJlZGlyZWN0X3VyaRgEIAEoCRIOCgZpc3N1ZXIYBSABKAkSNwoIcmVzb3VyY2UYBiABKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SDgoGc2NvcGVzGAcgAygJIisKEkdldE1jcFNldHVwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJIr8CChNHZXRNY3BTZXR1cFJlc3BvbnNlEg8KB2VuYWJsZWQYASABKAgSOwoNcmVnaXN0cmF0aW9ucxgCIAMoCzIkLmVuZ3JhbS5hcHAudjEuTWNwQ2xpZW50UmVnaXN0cmF0aW9uEjsKDGRlc3RpbmF0aW9ucxgDIAMoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92ZWREZXN0aW5hdGlvbhI1CgdjdXJyZW50GAQgASgLMiQuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsUmVzcG9uc2USGgoSdHJhbnNpdGlvbl9wZW5kaW5nGAUgASgIEg8KB2Zsb3dfaWQYBiABKAkSFwoPZmxvd19leHBpcmVzX2F0GAcgASgJEiAKGGNsaWVudF9zZWNyZXRfY29uZmlndXJlZBgIIAEoCCLUAQofQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDwoHZmxvd19pZBgEIAEoCRINCgVzdGF0ZRgFIAEoCRIMCgRjb2RlGAYgASgJEhMKBmlzc3VlchgHIAEoCUgAiAEBEhQKDHJlZGlyZWN0X3VyaRgIIAEoCUIJCgdfaXNzdWVyImwKG1JlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0y8BUKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USYAoQUHV0TWNwQ3JlZGVudGlhbBImLmVuZ3JhbS5hcHAudjEuUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxSZXNwb25zZRJUCgtHZXRNY3BTZXR1cBIhLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRNY3BTZXR1cFJlc3BvbnNlEnAKGENvbXBsZXRlTWNwQXV0aG9yaXphdGlvbhIuLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFJlc3BvbnNlEmgKFFJlY292ZXJNY3BDcmVkZW50aWFsEiouZW5ncmFtLmFwcC52MS5SZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxSZXNwb25zZRJXCgxBdXRob3JpemVNY3ASIi5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlc3BvbnNlEloKDURpc2Nvbm5lY3RNY3ASIy5lbmdyYW0uYXBwLnYxLkRpc2Nvbm5lY3RNY3BSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVzcG9uc2USYwoQTGlzdE1jcEFwcHJvdmFscxImLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRJmChFEZWNpZGVNY3BBcHByb3ZhbBInLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEloKDUNhbmNlbE1jcENhbGwSIy5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVzcG9uc2USZgoRUmVzb2x2ZU1jcE91dGNvbWUSJy5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRJOCglSZWJpbmRNY3ASHy5lbmdyYW0uYXBwLnYxLlJlYmluZE1jcFJlcXVlc3QaIC5lbmdyYW0uYXBwLnYxLlJlYmluZE1jcFJlc3BvbnNlYgZwcm90bzM"); + fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIo8DCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkSGgoSb3JpZ2luYWxfYXJndW1lbnRzGAogASgJEi0KDXJldmlld2VkX3Rvb2wYCyABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgMIAMoCRJIChdleHBlY3RlZF9yZXZpZXdfY29udGV4dBgNIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IkkKF0xpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSGgoSYWZ0ZXJfZXhlY3V0aW9uX2lkGAIgASgJImQKGExpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRItCglhcHByb3ZhbHMYASADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhkKEW5leHRfZXhlY3V0aW9uX2lkGAIgASgJIosBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgSSAoXZXhwZWN0ZWRfcmV2aWV3X2NvbnRleHQYAyABKAsyJy5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dCJeChlEZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJEjIKB3JlY2VpcHQYAiABKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmVjZWlwdCIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAkilQEKF1B1dE1jcENyZWRlbnRpYWxSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAiABKA0SGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgDIAEoDRIMCgRraW5kGAQgASgJEg4KBmhlYWRlchgFIAEoCRINCgV2YWx1ZRgGIAEoCSKUAQoTTWNwQ3JlZGVudGlhbFN0YXR1cxIOCgZzdGF0dXMYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJEgwKBGtpbmQYAyABKAkSGwoTY3JlZGVudGlhbF9pZGVudGl0eRgEIAEoCRIPCgd2ZXJzaW9uGAUgASgDEh4KFnJlc2VydmF0aW9uX2dlbmVyYXRpb24YBiABKAMijAEKGFB1dE1jcENyZWRlbnRpYWxSZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyKUAQogQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMikAEKHFJlY292ZXJNY3BDcmVkZW50aWFsUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMihgEKEk1jcENyZWRlbnRpYWxTdGF0ZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyI7ChZNY3BBcHByb3ZlZERlc3RpbmF0aW9uEg8KB3pvbmVfaWQYASABKAkSEAoIZW5kcG9pbnQYAiABKAkiuwEKFU1jcENsaWVudFJlZ2lzdHJhdGlvbhIKCgJpZBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSFAoMY29uZmlkZW50aWFsGAMgASgIEhQKDHJlZGlyZWN0X3VyaRgEIAEoCRIOCgZpc3N1ZXIYBSABKAkSNwoIcmVzb3VyY2UYBiABKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SDgoGc2NvcGVzGAcgAygJIisKEkdldE1jcFNldHVwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJIrwCChNHZXRNY3BTZXR1cFJlc3BvbnNlEg8KB2VuYWJsZWQYASABKAgSOwoNcmVnaXN0cmF0aW9ucxgCIAMoCzIkLmVuZ3JhbS5hcHAudjEuTWNwQ2xpZW50UmVnaXN0cmF0aW9uEjsKDGRlc3RpbmF0aW9ucxgDIAMoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92ZWREZXN0aW5hdGlvbhIyCgdjdXJyZW50GAQgASgLMiEuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdGUSGgoSdHJhbnNpdGlvbl9wZW5kaW5nGAUgASgIEg8KB2Zsb3dfaWQYBiABKAkSFwoPZmxvd19leHBpcmVzX2F0GAcgASgJEiAKGGNsaWVudF9zZWNyZXRfY29uZmlndXJlZBgIIAEoCCLUAQofQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDwoHZmxvd19pZBgEIAEoCRINCgVzdGF0ZRgFIAEoCRIMCgRjb2RlGAYgASgJEhMKBmlzc3VlchgHIAEoCUgAiAEBEhQKDHJlZGlyZWN0X3VyaRgIIAEoCUIJCgdfaXNzdWVyImwKG1JlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0i6gMKGE1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhYKDnJlcXVlc3RfZGlnZXN0GAUgASgJEhUKDWFyZ3VtZW50X2hhc2gYBiABKAkSFAoMYmluZGluZ19oYXNoGAcgASgJEhUKDWNvbm5lY3Rpb25faWQYCCABKAkSEgoKY2F0YWxvZ19pZBgJIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGAogASgNEhcKD2NvbmZpZ19yZXZpc2lvbhgLIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDCABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgNIAEoCRIVCg11cHN0cmVhbV9uYW1lGA4gASgJEhgKEHRvb2xfZmluZ2VycHJpbnQYDyABKAkSFgoOY29udGV4dF9kaWdlc3QYECABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgRIAEoDRISCgpleHBpcmVzX2F0GBIgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgTIAEoCRIVCg1iaW5kaW5nX2Vwb2NoGBQgASgJIr8BChJNY3BBcHByb3ZhbFJlY2VpcHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhAKCGRlY2lzaW9uGAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKCmRlY2lkZWRfYXQYBCABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgFIAEoDRJBChByZXZpZXdlZF9jb250ZXh0GAYgASgLMicuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbFJldmlld0NvbnRleHQi2AMKEk1jcEV4ZWN1dGlvblJldmlldxIUCgxleGVjdXRpb25faWQYASABKAkSFQoNY29ubmVjdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRIYChBqb3VybmFsX3JldmlzaW9uGAQgASgNEhoKEnJlc3VsdF91bmF2YWlsYWJsZRgFIAEoCBIqCgZyZXZpZXcYBiABKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhcKD2FsbG93ZWRfYWN0aW9ucxgHIAMoCRIOCgZyZWFzb24YCCABKAkSFAoMdG9vbF9jYWxsX2lkGAkgASgJEg4KBnJ1bl9pZBgKIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgLIAEoCRIVCg11cHN0cmVhbV9uYW1lGAwgASgJEhkKEWFwcHJvdmFsX2RlY2lzaW9uGA0gASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgOIAEoCRItCg1yZXZpZXdlZF90b29sGA8gASgLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YECADKAkSFAoMYmluZGluZ19oYXNoGBEgASgJImMKFE1jcFJlc29sdXRpb25SZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRIPCgdvdXRjb21lGAMgASgJEhIKCmNyZWF0ZWRfYXQYBCABKAkigQEKEU1jcEJpbmRpbmdSZWNlaXB0EhQKDGJpbmRpbmdfaGFzaBgBIAEoCRIVCg1iaW5kaW5nX2Vwb2NoGAIgASgJEg8KB2N1cnJlbnQYAyABKAgSFQoNc3RhbGVfcmVhc29ucxgEIAMoCRIXCg9hbGxvd2VkX2FjdGlvbnMYBSADKAkigAIKGUdldFNlc3Npb25NY3BTdGF0ZVJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIaChJhZnRlcl9leGVjdXRpb25faWQYAiABKAkSEQoJcGFnZV9zaXplGAMgASgNEh4KEXBsYW5fdG9vbF9jYWxsX2lkGAQgASgJSACIAQESHwoXYWZ0ZXJfYmluZGluZ190b29sX25hbWUYBSABKAkSGQoRYmluZGluZ19wYWdlX3NpemUYBiABKA0SFQoNb21pdF9iaW5kaW5ncxgHIAEoCBIXCg9vbWl0X2V4ZWN1dGlvbnMYCCABKAhCFAoSX3BsYW5fdG9vbF9jYWxsX2lkIr8EChpHZXRTZXNzaW9uTWNwU3RhdGVSZXNwb25zZRI5Cg9iaW5kaW5nX3JlY2VpcHQYASABKAsyIC5lbmdyYW0uYXBwLnYxLk1jcEJpbmRpbmdSZWNlaXB0EjUKCmV4ZWN1dGlvbnMYAiADKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEV4ZWN1dGlvblJldmlldxItCglhcHByb3ZhbHMYAyADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhwKFHVua25vd25fd3JpdGVfYmxvY2tzGAQgAygJEkAKE3Jlc29sdXRpb25fcmVjZWlwdHMYBSADKAsyIy5lbmdyYW0uYXBwLnYxLk1jcFJlc29sdXRpb25SZWNlaXB0EhcKD2FsbG93ZWRfYWN0aW9ucxgGIAMoCRIUCgxhdmFpbGFiaWxpdHkYByABKAkSGQoRbmV4dF9leGVjdXRpb25faWQYCCABKAkSPgoMd3JpdGVfcG9saWN5GAkgASgLMiguZW5ncmFtLmFwcC52MS5HZXRNY3BXcml0ZVBvbGljeVJlc3BvbnNlEjkKDWJpbmRpbmdfdG9vbHMYCiADKAsyIi5lbmdyYW0uYXBwLnYxLk1jcEJvdW5kVG9vbFN1bW1hcnkSHgoWbmV4dF9iaW5kaW5nX3Rvb2xfbmFtZRgLIAEoCRIeChZoYXNfbW9yZV9iaW5kaW5nX3Rvb2xzGAwgASgIEhsKE2hhc19tb3JlX2V4ZWN1dGlvbnMYDSABKAgiygIKE01jcEJvdW5kVG9vbFN1bW1hcnkSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIYChBjb25uZWN0aW9uX2FsaWFzGAIgASgJEhcKD21vZGVsX3Rvb2xfbmFtZRgDIAEoCRItCg1yZXZpZXdlZF90b29sGAQgASgLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YBSADKAkSGAoQYXV0b21hdGlvbl93cml0ZRgGIAEoCBIUCgx1bnJlc3RyaWN0ZWQYByABKAgSEgoKY2F0YWxvZ19pZBgIIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGAkgASgNEhcKD2NvbmZpZ19yZXZpc2lvbhgKIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YCyABKA0y8BYKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USYwoQUHV0TWNwQ3JlZGVudGlhbBImLmVuZ3JhbS5hcHAudjEuUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLlB1dE1jcENyZWRlbnRpYWxSZXNwb25zZRJUCgtHZXRNY3BTZXR1cBIhLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRNY3BTZXR1cFJlc3BvbnNlEnsKGENvbXBsZXRlTWNwQXV0aG9yaXphdGlvbhIuLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBovLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVzcG9uc2USbwoUUmVjb3Zlck1jcENyZWRlbnRpYWwSKi5lbmdyYW0uYXBwLnYxLlJlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuUmVjb3Zlck1jcENyZWRlbnRpYWxSZXNwb25zZRJXCgxBdXRob3JpemVNY3ASIi5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlc3BvbnNlEloKDURpc2Nvbm5lY3RNY3ASIy5lbmdyYW0uYXBwLnYxLkRpc2Nvbm5lY3RNY3BSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVzcG9uc2USaQoSR2V0U2Vzc2lvbk1jcFN0YXRlEiguZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXF1ZXN0GikuZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXNwb25zZRJjChBMaXN0TWNwQXBwcm92YWxzEiYuZW5ncmFtLmFwcC52MS5MaXN0TWNwQXBwcm92YWxzUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1Jlc3BvbnNlEmYKEURlY2lkZU1jcEFwcHJvdmFsEicuZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLkRlY2lkZU1jcEFwcHJvdmFsUmVzcG9uc2USWgoNQ2FuY2VsTWNwQ2FsbBIjLmVuZ3JhbS5hcHAudjEuQ2FuY2VsTWNwQ2FsbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXNwb25zZRJmChFSZXNvbHZlTWNwT3V0Y29tZRInLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5SZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEk4KCVJlYmluZE1jcBIfLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVzcG9uc2ViBnByb3RvMw", [file_engram_app_v1_session]); /** * @generated from message engram.app.v1.GoogleCloudConnectionConfig @@ -1493,6 +1495,26 @@ export type McpApproval = Message<"engram.app.v1.McpApproval"> & { * @generated from field: string expires_at = 9; */ expiresAt: string; + + /** + * @generated from field: string original_arguments = 10; + */ + originalArguments: string; + + /** + * @generated from field: engram.app.v1.McpTool reviewed_tool = 11; + */ + reviewedTool?: McpTool; + + /** + * @generated from field: repeated string argument_constraint_documents_json = 12; + */ + argumentConstraintDocumentsJson: string[]; + + /** + * @generated from field: engram.app.v1.McpApprovalReviewContext expected_review_context = 13; + */ + expectedReviewContext?: McpApprovalReviewContext; }; /** @@ -1510,6 +1532,11 @@ export type ListMcpApprovalsRequest = Message<"engram.app.v1.ListMcpApprovalsReq * @generated from field: string session_id = 1; */ sessionId: string; + + /** + * @generated from field: string after_execution_id = 2; + */ + afterExecutionId: string; }; /** @@ -1527,6 +1554,11 @@ export type ListMcpApprovalsResponse = Message<"engram.app.v1.ListMcpApprovalsRe * @generated from field: repeated engram.app.v1.McpApproval approvals = 1; */ approvals: McpApproval[]; + + /** + * @generated from field: string next_execution_id = 2; + */ + nextExecutionId: string; }; /** @@ -1549,6 +1581,11 @@ export type DecideMcpApprovalRequest = Message<"engram.app.v1.DecideMcpApprovalR * @generated from field: bool approve = 2; */ approve: boolean; + + /** + * @generated from field: engram.app.v1.McpApprovalReviewContext expected_review_context = 3; + */ + expectedReviewContext?: McpApprovalReviewContext; }; /** @@ -1566,6 +1603,11 @@ export type DecideMcpApprovalResponse = Message<"engram.app.v1.DecideMcpApproval * @generated from field: string state = 1; */ state: string; + + /** + * @generated from field: engram.app.v1.McpApprovalReceipt receipt = 2; + */ + receipt?: McpApprovalReceipt; }; /** @@ -1774,9 +1816,9 @@ export const McpCredentialStatusSchema: GenMessage = /*@__P messageDesc(file_engram_app_v1_integration, 61); /** - * @generated from message engram.app.v1.McpCredentialResponse + * @generated from message engram.app.v1.PutMcpCredentialResponse */ -export type McpCredentialResponse = Message<"engram.app.v1.McpCredentialResponse"> & { +export type PutMcpCredentialResponse = Message<"engram.app.v1.PutMcpCredentialResponse"> & { /** * @generated from field: engram.app.v1.IntegrationConnection connection = 1; */ @@ -1789,12 +1831,78 @@ export type McpCredentialResponse = Message<"engram.app.v1.McpCredentialResponse }; /** - * Describes the message engram.app.v1.McpCredentialResponse. - * Use `create(McpCredentialResponseSchema)` to create a new message. + * Describes the message engram.app.v1.PutMcpCredentialResponse. + * Use `create(PutMcpCredentialResponseSchema)` to create a new message. */ -export const McpCredentialResponseSchema: GenMessage = /*@__PURE__*/ +export const PutMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_integration, 62); +/** + * @generated from message engram.app.v1.CompleteMcpAuthorizationResponse + */ +export type CompleteMcpAuthorizationResponse = Message<"engram.app.v1.CompleteMcpAuthorizationResponse"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; + + /** + * @generated from field: engram.app.v1.McpCredentialStatus credential = 2; + */ + credential?: McpCredentialStatus; +}; + +/** + * Describes the message engram.app.v1.CompleteMcpAuthorizationResponse. + * Use `create(CompleteMcpAuthorizationResponseSchema)` to create a new message. + */ +export const CompleteMcpAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 63); + +/** + * @generated from message engram.app.v1.RecoverMcpCredentialResponse + */ +export type RecoverMcpCredentialResponse = Message<"engram.app.v1.RecoverMcpCredentialResponse"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; + + /** + * @generated from field: engram.app.v1.McpCredentialStatus credential = 2; + */ + credential?: McpCredentialStatus; +}; + +/** + * Describes the message engram.app.v1.RecoverMcpCredentialResponse. + * Use `create(RecoverMcpCredentialResponseSchema)` to create a new message. + */ +export const RecoverMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 64); + +/** + * @generated from message engram.app.v1.McpCredentialState + */ +export type McpCredentialState = Message<"engram.app.v1.McpCredentialState"> & { + /** + * @generated from field: engram.app.v1.IntegrationConnection connection = 1; + */ + connection?: IntegrationConnection; + + /** + * @generated from field: engram.app.v1.McpCredentialStatus credential = 2; + */ + credential?: McpCredentialStatus; +}; + +/** + * Describes the message engram.app.v1.McpCredentialState. + * Use `create(McpCredentialStateSchema)` to create a new message. + */ +export const McpCredentialStateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 65); + /** * @generated from message engram.app.v1.McpApprovedDestination */ @@ -1815,7 +1923,7 @@ export type McpApprovedDestination = Message<"engram.app.v1.McpApprovedDestinati * Use `create(McpApprovedDestinationSchema)` to create a new message. */ export const McpApprovedDestinationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 63); + messageDesc(file_engram_app_v1_integration, 66); /** * @generated from message engram.app.v1.McpClientRegistration @@ -1862,7 +1970,7 @@ export type McpClientRegistration = Message<"engram.app.v1.McpClientRegistration * Use `create(McpClientRegistrationSchema)` to create a new message. */ export const McpClientRegistrationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 64); + messageDesc(file_engram_app_v1_integration, 67); /** * @generated from message engram.app.v1.GetMcpSetupRequest @@ -1879,7 +1987,7 @@ export type GetMcpSetupRequest = Message<"engram.app.v1.GetMcpSetupRequest"> & { * Use `create(GetMcpSetupRequestSchema)` to create a new message. */ export const GetMcpSetupRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 65); + messageDesc(file_engram_app_v1_integration, 68); /** * @generated from message engram.app.v1.GetMcpSetupResponse @@ -1901,9 +2009,9 @@ export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & destinations: McpApprovedDestination[]; /** - * @generated from field: engram.app.v1.McpCredentialResponse current = 4; + * @generated from field: engram.app.v1.McpCredentialState current = 4; */ - current?: McpCredentialResponse; + current?: McpCredentialState; /** * @generated from field: bool transition_pending = 5; @@ -1931,7 +2039,7 @@ export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & * Use `create(GetMcpSetupResponseSchema)` to create a new message. */ export const GetMcpSetupResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 66); + messageDesc(file_engram_app_v1_integration, 69); /** * @generated from message engram.app.v1.CompleteMcpAuthorizationRequest @@ -1983,7 +2091,7 @@ export type CompleteMcpAuthorizationRequest = Message<"engram.app.v1.CompleteMcp * Use `create(CompleteMcpAuthorizationRequestSchema)` to create a new message. */ export const CompleteMcpAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 67); + messageDesc(file_engram_app_v1_integration, 70); /** * @generated from message engram.app.v1.RecoverMcpCredentialRequest @@ -2010,7 +2118,527 @@ export type RecoverMcpCredentialRequest = Message<"engram.app.v1.RecoverMcpCrede * Use `create(RecoverMcpCredentialRequestSchema)` to create a new message. */ export const RecoverMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 68); + messageDesc(file_engram_app_v1_integration, 71); + +/** + * Exact server review. Clients must return it unchanged when they decide. + * + * @generated from message engram.app.v1.McpApprovalReviewContext + */ +export type McpApprovalReviewContext = Message<"engram.app.v1.McpApprovalReviewContext"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 4; + */ + toolCallId: string; + + /** + * @generated from field: string request_digest = 5; + */ + requestDigest: string; + + /** + * @generated from field: string argument_hash = 6; + */ + argumentHash: string; + + /** + * @generated from field: string binding_hash = 7; + */ + bindingHash: string; + + /** + * @generated from field: string connection_id = 8; + */ + connectionId: string; + + /** + * @generated from field: string catalog_id = 9; + */ + catalogId: string; + + /** + * @generated from field: uint32 catalog_revision = 10; + */ + catalogRevision: number; + + /** + * @generated from field: uint32 config_revision = 11; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 12; + */ + authGeneration: number; + + /** + * @generated from field: string credential_identity = 13; + */ + credentialIdentity: string; + + /** + * @generated from field: string upstream_name = 14; + */ + upstreamName: string; + + /** + * @generated from field: string tool_fingerprint = 15; + */ + toolFingerprint: string; + + /** + * @generated from field: string context_digest = 16; + */ + contextDigest: string; + + /** + * @generated from field: uint32 journal_revision = 17; + */ + journalRevision: number; + + /** + * @generated from field: string expires_at = 18; + */ + expiresAt: string; + + /** + * @generated from field: string policy_revision = 19; + */ + policyRevision: string; + + /** + * @generated from field: string binding_epoch = 20; + */ + bindingEpoch: string; +}; + +/** + * Describes the message engram.app.v1.McpApprovalReviewContext. + * Use `create(McpApprovalReviewContextSchema)` to create a new message. + */ +export const McpApprovalReviewContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 72); + +/** + * @generated from message engram.app.v1.McpApprovalReceipt + */ +export type McpApprovalReceipt = Message<"engram.app.v1.McpApprovalReceipt"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string decision = 2; + */ + decision: string; + + /** + * @generated from field: string actor_id = 3; + */ + actorId: string; + + /** + * @generated from field: string decided_at = 4; + */ + decidedAt: string; + + /** + * @generated from field: uint32 journal_revision = 5; + */ + journalRevision: number; + + /** + * @generated from field: engram.app.v1.McpApprovalReviewContext reviewed_context = 6; + */ + reviewedContext?: McpApprovalReviewContext; +}; + +/** + * Describes the message engram.app.v1.McpApprovalReceipt. + * Use `create(McpApprovalReceiptSchema)` to create a new message. + */ +export const McpApprovalReceiptSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 73); + +/** + * @generated from message engram.app.v1.McpExecutionReview + */ +export type McpExecutionReview = Message<"engram.app.v1.McpExecutionReview"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string connection_id = 2; + */ + connectionId: string; + + /** + * @generated from field: string state = 3; + */ + state: string; + + /** + * @generated from field: uint32 journal_revision = 4; + */ + journalRevision: number; + + /** + * @generated from field: bool result_unavailable = 5; + */ + resultUnavailable: boolean; + + /** + * @generated from field: engram.app.v1.McpApproval review = 6; + */ + review?: McpApproval; + + /** + * @generated from field: repeated string allowed_actions = 7; + */ + allowedActions: string[]; + + /** + * @generated from field: string reason = 8; + */ + reason: string; + + /** + * @generated from field: string tool_call_id = 9; + */ + toolCallId: string; + + /** + * @generated from field: string run_id = 10; + */ + runId: string; + + /** + * @generated from field: string request_digest = 11; + */ + requestDigest: string; + + /** + * @generated from field: string upstream_name = 12; + */ + upstreamName: string; + + /** + * @generated from field: string approval_decision = 13; + */ + approvalDecision: string; + + /** + * @generated from field: string original_arguments = 14; + */ + originalArguments: string; + + /** + * @generated from field: engram.app.v1.McpTool reviewed_tool = 15; + */ + reviewedTool?: McpTool; + + /** + * @generated from field: repeated string argument_constraint_documents_json = 16; + */ + argumentConstraintDocumentsJson: string[]; + + /** + * @generated from field: string binding_hash = 17; + */ + bindingHash: string; +}; + +/** + * Describes the message engram.app.v1.McpExecutionReview. + * Use `create(McpExecutionReviewSchema)` to create a new message. + */ +export const McpExecutionReviewSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 74); + +/** + * @generated from message engram.app.v1.McpResolutionReceipt + */ +export type McpResolutionReceipt = Message<"engram.app.v1.McpResolutionReceipt"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string actor_id = 2; + */ + actorId: string; + + /** + * @generated from field: string outcome = 3; + */ + outcome: string; + + /** + * @generated from field: string created_at = 4; + */ + createdAt: string; +}; + +/** + * Describes the message engram.app.v1.McpResolutionReceipt. + * Use `create(McpResolutionReceiptSchema)` to create a new message. + */ +export const McpResolutionReceiptSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 75); + +/** + * @generated from message engram.app.v1.McpBindingReceipt + */ +export type McpBindingReceipt = Message<"engram.app.v1.McpBindingReceipt"> & { + /** + * @generated from field: string binding_hash = 1; + */ + bindingHash: string; + + /** + * @generated from field: string binding_epoch = 2; + */ + bindingEpoch: string; + + /** + * @generated from field: bool current = 3; + */ + current: boolean; + + /** + * @generated from field: repeated string stale_reasons = 4; + */ + staleReasons: string[]; + + /** + * @generated from field: repeated string allowed_actions = 5; + */ + allowedActions: string[]; +}; + +/** + * Describes the message engram.app.v1.McpBindingReceipt. + * Use `create(McpBindingReceiptSchema)` to create a new message. + */ +export const McpBindingReceiptSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 76); + +/** + * @generated from message engram.app.v1.GetSessionMcpStateRequest + */ +export type GetSessionMcpStateRequest = Message<"engram.app.v1.GetSessionMcpStateRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string after_execution_id = 2; + */ + afterExecutionId: string; + + /** + * @generated from field: uint32 page_size = 3; + */ + pageSize: number; + + /** + * @generated from field: optional string plan_tool_call_id = 4; + */ + planToolCallId?: string; + + /** + * @generated from field: string after_binding_tool_name = 5; + */ + afterBindingToolName: string; + + /** + * @generated from field: uint32 binding_page_size = 6; + */ + bindingPageSize: number; + + /** + * @generated from field: bool omit_bindings = 7; + */ + omitBindings: boolean; + + /** + * @generated from field: bool omit_executions = 8; + */ + omitExecutions: boolean; +}; + +/** + * Describes the message engram.app.v1.GetSessionMcpStateRequest. + * Use `create(GetSessionMcpStateRequestSchema)` to create a new message. + */ +export const GetSessionMcpStateRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 77); + +/** + * @generated from message engram.app.v1.GetSessionMcpStateResponse + */ +export type GetSessionMcpStateResponse = Message<"engram.app.v1.GetSessionMcpStateResponse"> & { + /** + * @generated from field: engram.app.v1.McpBindingReceipt binding_receipt = 1; + */ + bindingReceipt?: McpBindingReceipt; + + /** + * @generated from field: repeated engram.app.v1.McpExecutionReview executions = 2; + */ + executions: McpExecutionReview[]; + + /** + * @generated from field: repeated engram.app.v1.McpApproval approvals = 3; + */ + approvals: McpApproval[]; + + /** + * @generated from field: repeated string unknown_write_blocks = 4; + */ + unknownWriteBlocks: string[]; + + /** + * @generated from field: repeated engram.app.v1.McpResolutionReceipt resolution_receipts = 5; + */ + resolutionReceipts: McpResolutionReceipt[]; + + /** + * @generated from field: repeated string allowed_actions = 6; + */ + allowedActions: string[]; + + /** + * @generated from field: string availability = 7; + */ + availability: string; + + /** + * @generated from field: string next_execution_id = 8; + */ + nextExecutionId: string; + + /** + * @generated from field: engram.app.v1.GetMcpWritePolicyResponse write_policy = 9; + */ + writePolicy?: GetMcpWritePolicyResponse; + + /** + * @generated from field: repeated engram.app.v1.McpBoundToolSummary binding_tools = 10; + */ + bindingTools: McpBoundToolSummary[]; + + /** + * @generated from field: string next_binding_tool_name = 11; + */ + nextBindingToolName: string; + + /** + * @generated from field: bool has_more_binding_tools = 12; + */ + hasMoreBindingTools: boolean; + + /** + * @generated from field: bool has_more_executions = 13; + */ + hasMoreExecutions: boolean; +}; + +/** + * Describes the message engram.app.v1.GetSessionMcpStateResponse. + * Use `create(GetSessionMcpStateResponseSchema)` to create a new message. + */ +export const GetSessionMcpStateResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 78); + +/** + * Immutable session grant. Schemas remain independent documents with their own roots. + * + * @generated from message engram.app.v1.McpBoundToolSummary + */ +export type McpBoundToolSummary = Message<"engram.app.v1.McpBoundToolSummary"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: string connection_alias = 2; + */ + connectionAlias: string; + + /** + * @generated from field: string model_tool_name = 3; + */ + modelToolName: string; + + /** + * @generated from field: engram.app.v1.McpTool reviewed_tool = 4; + */ + reviewedTool?: McpTool; + + /** + * @generated from field: repeated string argument_constraint_documents_json = 5; + */ + argumentConstraintDocumentsJson: string[]; + + /** + * @generated from field: bool automation_write = 6; + */ + automationWrite: boolean; + + /** + * @generated from field: bool unrestricted = 7; + */ + unrestricted: boolean; + + /** + * @generated from field: string catalog_id = 8; + */ + catalogId: string; + + /** + * @generated from field: uint32 catalog_revision = 9; + */ + catalogRevision: number; + + /** + * @generated from field: uint32 config_revision = 10; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 11; + */ + authGeneration: number; +}; + +/** + * Describes the message engram.app.v1.McpBoundToolSummary. + * Use `create(McpBoundToolSummarySchema)` to create a new message. + */ +export const McpBoundToolSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 79); /** * ADR 0057 C3: orchestrator-native connector catalog CRUD (Plane B). Connectors @@ -2202,7 +2830,7 @@ export const IntegrationService: GenService<{ putMcpCredential: { methodKind: "unary"; input: typeof PutMcpCredentialRequestSchema; - output: typeof McpCredentialResponseSchema; + output: typeof PutMcpCredentialResponseSchema; }, /** * @generated from rpc engram.app.v1.IntegrationService.GetMcpSetup @@ -2218,7 +2846,7 @@ export const IntegrationService: GenService<{ completeMcpAuthorization: { methodKind: "unary"; input: typeof CompleteMcpAuthorizationRequestSchema; - output: typeof McpCredentialResponseSchema; + output: typeof CompleteMcpAuthorizationResponseSchema; }, /** * @generated from rpc engram.app.v1.IntegrationService.RecoverMcpCredential @@ -2226,7 +2854,7 @@ export const IntegrationService: GenService<{ recoverMcpCredential: { methodKind: "unary"; input: typeof RecoverMcpCredentialRequestSchema; - output: typeof McpCredentialResponseSchema; + output: typeof RecoverMcpCredentialResponseSchema; }, /** * @generated from rpc engram.app.v1.IntegrationService.AuthorizeMcp @@ -2244,6 +2872,14 @@ export const IntegrationService: GenService<{ input: typeof DisconnectMcpRequestSchema; output: typeof DisconnectMcpResponseSchema; }, + /** + * @generated from rpc engram.app.v1.IntegrationService.GetSessionMcpState + */ + getSessionMcpState: { + methodKind: "unary"; + input: typeof GetSessionMcpStateRequestSchema; + output: typeof GetSessionMcpStateResponseSchema; + }, /** * @generated from rpc engram.app.v1.IntegrationService.ListMcpApprovals */ From 1d708a27a02976f47e361198a1364408ec825c05 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 03:31:08 -0500 Subject: [PATCH 021/279] Bind MCP resource calls to verified worker channels --- mcp-worker/README.md | 20 +- mcp-worker/src/service.ts | 6 +- mcp-worker/src/validation.ts | 9 +- mcp-worker/test/contracts.test.ts | 58 +- .../test/fixtures/resource-client-worker.ts | 80 +++ mcp-worker/test/fixtures/worker-tls.ts | 6 +- mcp-worker/test/service.test.ts | 58 ++ .../src/__tests__/mcp-worker-client.test.ts | 540 ++++++++++++++++++ .../src/__tests__/mcp-worker-encoding.test.ts | 34 ++ orchestrator/src/mcp/worker-client.ts | 316 ++++++++++ orchestrator/src/mcp/worker-encoding.ts | 27 + 11 files changed, 1131 insertions(+), 23 deletions(-) create mode 100644 mcp-worker/test/fixtures/resource-client-worker.ts create mode 100644 orchestrator/src/__tests__/mcp-worker-client.test.ts create mode 100644 orchestrator/src/__tests__/mcp-worker-encoding.test.ts create mode 100644 orchestrator/src/mcp/worker-client.ts create mode 100644 orchestrator/src/mcp/worker-encoding.ts diff --git a/mcp-worker/README.md b/mcp-worker/README.md index 19cb520b0..bf4dce64e 100644 --- a/mcp-worker/README.md +++ b/mcp-worker/README.md @@ -41,12 +41,26 @@ bearer fallback, generic proxy, or guest listener is provided. is forwarded to a provider. - Execution scope contains the real session, run, tool-call, and immutable binding hash. Administration scope contains the trusted request and actor - identities. Execute requires execution scope. Discovery, schema validation, - destination validation, and OAuth require administration scope. Cancel must + identities. Execute requires execution scope. Schema validation accepts either + scope, has no credential or destination field, and requires the orchestrator + caller role. Discovery, destination validation, and OAuth require administration scope. Cancel must match the original scope, caller, connection revision, and credential identity. Before acquiring or sending a provider credential, the trusted client must verify the worker identity against the approved zone and frozen policy digest. +The resource client in `orchestrator/src/mcp/worker-client.ts` retains that exact +TLS socket for one RPC. Create one client per operation with its cancellation +signal, and dispose it after use. An unused identity check can be replaced only +before credential acquisition. A closed socket cannot reconnect for dispatch. +The server permits two requests per socket and at most 35 seconds of idle time. +The client verifies the server CA, hostname, certificate pin, protocol, zone, +policy, and boot identity. It does not use a proxy, follow redirects, or retry. + +The session binding uses `sha256:` digests. The worker scope uses bare hexadecimal +for its binding hash. Use the strict conversion in `worker-encoding.ts`; do not +change the frozen binding or journal digest. Worker policy digests and coordinator +credential receipts stay in their original bare hexadecimal form. + It must authorize current platform state and acquire durable global admission. Then it creates a new intent bound to that worker boot, policy, connection revision, credential identity, operation, exact destination, scope, expiry, @@ -92,7 +106,7 @@ Limits include 256 KiB arguments, 1 MiB catalog and normalized output, 2 MiB 60 seconds per tool by default, and a 300-second ceiling. Outer deadlines and abort signals cover all I/O. Local active caps are 4 per session, 8 per connection, and 32 executions per worker. Discovery has two slots, OAuth four, -and schema/destination administration four. Schema compilation also has a +and schema/destination checks four. Schema compilation also has a shared four-child cap, including children started by execute/discover. These local caps supplement durable admission across replicas; they do not replace it. diff --git a/mcp-worker/src/service.ts b/mcp-worker/src/service.ts index 90a552655..651c6d5c2 100644 --- a/mcp-worker/src/service.ts +++ b/mcp-worker/src/service.ts @@ -73,7 +73,11 @@ export function createWorker(options: WorkerOptions) { server.requestTimeout = 5000; server.headersTimeout = 5000; server.timeout = 305000; - server.maxRequestsPerSocket = 1; + // A resource client verifies identity, then sends one RPC on that same TLS socket. + // This keeps credentials on the verified worker boot, including during a roll. + server.maxRequestsPerSocket = 2; + server.keepAliveTimeout = 35000; + server.keepAliveTimeoutBuffer = 0; async function handle( req: IncomingMessage, ): Promise<{ status: number; body: unknown }> { diff --git a/mcp-worker/src/validation.ts b/mcp-worker/src/validation.ts index 0904742d0..d33539b66 100644 --- a/mcp-worker/src/validation.ts +++ b/mcp-worker/src/validation.ts @@ -46,7 +46,12 @@ function operationValid(value: unknown): value is WorkerOperation { case "cancel": return text(value.targetIntentId); case "validateSchemas": - return schemas(value.documents); + return ( + schemas(value.documents) && + Object.keys(value).every((key) => + ["kind", "documents", "value"].includes(key), + ) + ); case "execute": return ( text(value.tool) && @@ -128,7 +133,7 @@ export function parseWorkerRequest(value: unknown): WorkerRequest { ) throw Error("execution_scope_required"); if ( - !["execute", "cancel"].includes(value.operation.kind) && + !["execute", "cancel", "validateSchemas"].includes(value.operation.kind) && value.intent.scope.kind !== "administration" ) throw Error("administration_scope_required"); diff --git a/mcp-worker/test/contracts.test.ts b/mcp-worker/test/contracts.test.ts index 2e21d097f..bcb7eb503 100644 --- a/mcp-worker/test/contracts.test.ts +++ b/mcp-worker/test/contracts.test.ts @@ -38,21 +38,51 @@ test("wire contract has distinct administration and execution scopes", () => { }, }), ); - assert.throws(() => - parseWorkerRequest({ - ...request, - intent: { - ...request.intent, - scope: { - kind: "execution", - sessionId: "session", - runId: "run", - toolCallId: "call", - bindingHash: "d".repeat(64), - }, + const execution = { + ...request, + intent: { + ...request.intent, + scope: { + kind: "execution" as const, + sessionId: "session", + runId: "run", + toolCallId: "call", + bindingHash: "d".repeat(64), }, - }), - ); + }, + }; + assert.deepEqual(parseWorkerRequest(execution), execution); + for (const field of ["sessionId", "runId", "toolCallId", "bindingHash"]) { + assert.throws(() => + parseWorkerRequest({ + ...execution, + intent: { + ...execution.intent, + scope: { ...execution.intent.scope, [field]: "" }, + }, + }), + ); + } + for (const extra of [ + { credential: { header: "authorization", value: "Bearer private" } }, + { endpoint: "https://tools.example/mcp" }, + ]) { + assert.throws(() => + parseWorkerRequest({ + ...execution, + operation: { ...execution.operation, ...extra }, + }), + ); + } + for (const kind of ["discover", "validateDestination", "metadata"]) { + assert.throws(() => + parseWorkerRequest({ + ...execution, + intent: { ...execution.intent, operation: kind }, + operation: { kind, endpoint: "https://tools.example/mcp" }, + }), + ); + } assert.throws(() => parseWorkerRequest({ ...request, diff --git a/mcp-worker/test/fixtures/resource-client-worker.ts b/mcp-worker/test/fixtures/resource-client-worker.ts new file mode 100644 index 000000000..4b90d7329 --- /dev/null +++ b/mcp-worker/test/fixtures/resource-client-worker.ts @@ -0,0 +1,80 @@ +// Actual Node worker fixture. Admission receipts are test inputs, not database proof. +import { createInterface } from "node:readline"; +import { createServer } from "node:https"; +import { createWorker } from "../../src/service.ts"; +import { ZonePolicy } from "../../src/network.ts"; + +const lines = createInterface({ input: process.stdin }); +const input = lines[Symbol.asyncIterator](); +const first = await input.next(); +const tls = JSON.parse(first.value!); +let providerDials = 0; +const makeWorker = () => + createWorker({ + tls, + zone: new ZonePolicy({ + id: "public", + revision: "1", + mode: "public", + authorities: ["tools.example"], + aliases: [], + cidrs: [], + denyCidrs: [], + }), + callers: [{ fingerprint: tls.fingerprint, role: "orchestrator" }], + enabled: true, + network: { + resolve: async () => { + providerDials++; + throw new Error("fixture has no provider"); + }, + request: () => { + providerDials++; + throw new Error("fixture has no provider"); + }, + }, + }); +let worker = makeWorker(); +const port = await worker.listen(0, "127.0.0.1"); +const send = (value: unknown) => + process.stdout.write(`${JSON.stringify(value)}\n`); +send({ port, identity: worker.identity, node: process.versions.node }); +let replacement: ReturnType | undefined; +let replacementBytes = 0; +let replacementRequests = 0; +for await (const line of { [Symbol.asyncIterator]: () => input }) { + if (line === "restart") { + await worker.close(); + worker = makeWorker(); + await worker.listen(port, "127.0.0.1"); + send({ identity: worker.identity }); + } else if (line === "replace") { + await worker.close(); + replacement = createServer( + { ...tls, requestCert: true, rejectUnauthorized: true }, + (req, res) => { + replacementRequests++; + req.on("data", (chunk: Buffer) => { + replacementBytes += chunk.length; + }); + req.on("end", () => { + res.writeHead(500); + res.end(); + }); + }, + ); + await new Promise((resolve) => + replacement!.listen(port, "127.0.0.1", resolve), + ); + send({ replaced: true }); + } else if (line === "stats") { + send({ providerDials, replacementBytes, replacementRequests }); + } else if (line === "close") { + if (replacement) { + replacement.closeAllConnections(); + await new Promise((resolve) => replacement!.close(() => resolve())); + } else await worker.close(); + lines.close(); + break; + } +} diff --git a/mcp-worker/test/fixtures/worker-tls.ts b/mcp-worker/test/fixtures/worker-tls.ts index c4989fbc2..8435e6fca 100644 --- a/mcp-worker/test/fixtures/worker-tls.ts +++ b/mcp-worker/test/fixtures/worker-tls.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { execFileSync } from "node:child_process"; import { X509Certificate } from "node:crypto"; import { request } from "node:https"; -export async function workerTls() { +export async function workerTls(hostname = "tools.example") { const dir = await mkdtemp(join(tmpdir(), "mcp-worker-tls-")); try { execFileSync( @@ -22,9 +22,9 @@ export async function workerTls() { "-days", "1", "-subj", - "/CN=tools.example", + `/CN=${hostname}`, "-addext", - "subjectAltName=DNS:tools.example", + `subjectAltName=DNS:${hostname}${hostname === "localhost" ? ",IP:127.0.0.1" : ""}`, "-addext", "extendedKeyUsage=serverAuth,clientAuth", ], diff --git a/mcp-worker/test/service.test.ts b/mcp-worker/test/service.test.ts index b894993b1..2c7a0eae8 100644 --- a/mcp-worker/test/service.test.ts +++ b/mcp-worker/test/service.test.ts @@ -146,6 +146,64 @@ test("disabled worker and unknown client identity cannot dispatch", async (t) => t.after(() => unknown.close()); assert.equal((await localRpc(other, tls, "/identity")).status, 403); }); +test("execution schema validation keeps the orchestrator role and exact bound scope", async (t) => { + const tls = await workerTls(); + for (const role of ["orchestrator", "coordinator"] as const) { + let dials = 0; + const worker = createWorker({ + zone: zone(), + tls, + callers: [{ fingerprint: tls.fingerprint, role }], + enabled: true, + network: { + resolve: async () => { + dials++; + throw Error("unexpected provider lookup"); + }, + request: () => { + dials++; + throw Error("unexpected provider request"); + }, + }, + }); + const port = await worker.listen(0, "127.0.0.1"); + t.after(() => worker.close()); + const request = envelope(worker.identity, { + kind: "validateSchemas", + documents: [{ type: "integer" }], + value: 2, + }); + request.intent.scope = { + kind: "execution", + sessionId: "session", + runId: "run", + toolCallId: "call", + bindingHash: "b".repeat(64), + }; + const result = await localRpc(port, tls, "/rpc", request); + assert.equal(result.status, role === "orchestrator" ? 200 : 403); + if (role === "orchestrator") { + assert.ok( + typeof result.body === "object" && + result.body !== null && + "intent" in result.body, + ); + assert.deepEqual(result.body.intent, request.intent); + assert.equal((await localRpc(port, tls, "/rpc", request)).status, 403); + const credential = { header: "authorization", value: "Bearer private" }; + const secretRequest = envelope(worker.identity, { + kind: "validateSchemas", + documents: [{}], + }); + const invalid = { + ...secretRequest, + operation: { ...secretRequest.operation, credential }, + }; + assert.equal((await localRpc(port, tls, "/rpc", invalid)).status, 400); + } + assert.equal(dials, 0); + } +}); test("OAuth admission is separate, destination validation does not dial, and scoped cancel aborts I/O", async (t) => { const { createServer, request } = await import("node:https"); const tls = await workerTls(); diff --git a/orchestrator/src/__tests__/mcp-worker-client.test.ts b/orchestrator/src/__tests__/mcp-worker-client.test.ts new file mode 100644 index 000000000..a7da0d8a2 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-worker-client.test.ts @@ -0,0 +1,540 @@ +import { expect, test } from "bun:test"; +import { spawn } from "node:child_process"; +import { createInterface } from "node:readline"; +import { createServer } from "node:https"; +import type { IncomingMessage, ServerResponse } from "node:http"; +import { once } from "node:events"; +import { workerTls } from "../../../mcp-worker/test/fixtures/worker-tls.ts"; +import { digest } from "../../../mcp-worker/src/index.ts"; +import type { + WorkerIdentity, + WorkerOperation, + WorkerRequest, +} from "../../../mcp-worker/src/contracts.ts"; +import { makeResourceWorkerClient } from "../mcp/worker-client.ts"; +import { toWorkerBindingHash } from "../mcp/worker-encoding.ts"; + +function envelope(identity: WorkerIdentity, operation: WorkerOperation): WorkerRequest { + const expiresAt = Date.now() + 10000; + return { + protocol: 1, + intent: { + id: crypto.randomUUID(), + bootId: identity.bootId, + zoneId: identity.zoneId, + policyDigest: identity.policyDigest, + connectionId: "connection", + connectionRevision: 1, + credentialIdentity: "credential", + operation: operation.kind, + scope: { + kind: "execution", + sessionId: "session", + runId: "run", + toolCallId: "call", + bindingHash: toWorkerBindingHash(`sha256:${"b".repeat(64)}`), + }, + destination: "endpoint" in operation ? operation.endpoint : "", + requestDigest: `sha256:${"c".repeat(64)}`, + operationDigest: digest(operation), + expiresAt, + lease: { id: crypto.randomUUID(), deploymentId: "fixture-deployment", expiresAt }, + }, + operation, + }; +} +const schema: WorkerOperation = { + kind: "validateSchemas", + documents: [{ type: "integer" }], + value: 1, +}; +function options( + tls: Awaited>, + port: number, + identity: WorkerIdentity, +) { + return { + tls: { ...tls, url: `https://127.0.0.1:${port}`, serverFingerprint: tls.fingerprint }, + policy: { + zoneId: identity.zoneId, + policyRevision: identity.policyRevision, + policyDigest: `sha256:${identity.policyDigest}`, + }, + }; +} +async function actualWorker() { + const tls = await workerTls("localhost"); + const process = spawn( + globalThis.process.env.MCP_WORKER_TEST_NODE ?? "node", + [ + new URL("../../../mcp-worker/test/fixtures/resource-client-worker.ts", import.meta.url) + .pathname, + ], + { stdio: ["pipe", "pipe", "pipe"], env: { PATH: globalThis.process.env.PATH ?? "" } }, + ); + const output = createInterface({ input: process.stdout }); + const lines = output[Symbol.asyncIterator](); + let stderr = ""; + process.stderr.on("data", (data) => { + stderr += String(data); + }); + process.stdin.write(`${JSON.stringify(tls)}\n`); + async function next() { + const line = await lines.next(); + if (line.done) throw new Error(`worker fixture exited: ${stderr}`); + return JSON.parse(line.value); + } + const ready: { port: number; identity: WorkerIdentity; node: string } = await next(); + expect(ready.node.startsWith("24.")).toBe(true); + return { + ...ready, + tls, + async command(command: string) { + process.stdin.write(`${command}\n`); + return next(); + }, + async close() { + const exited = once(process, "exit"); + process.stdin.end("close\n"); + await exited; + output.close(); + expect(stderr).toBe(""); + }, + }; +} + +async function protocolFixture( + reply: (req: IncomingMessage, res: ServerResponse, body: WorkerRequest | undefined) => void, +) { + const tls = await workerTls("localhost"); + const identity: WorkerIdentity = { + protocol: 1, + bootId: "boot", + zoneId: "public", + policyRevision: "1", + policyDigest: "a".repeat(64), + }; + let rpcs = 0, + identities = 0, + bytes = 0; + const sockets = new Set(); + const server = createServer( + { ...tls, requestCert: true, rejectUnauthorized: true }, + (req, res) => { + sockets.add(req.socket); + if (req.url === "/identity") { + identities++; + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify(identity)); + } else { + rpcs++; + let text = ""; + req.on("data", (chunk: Buffer) => { + bytes += chunk.length; + text += chunk; + }); + req.on("end", () => reply(req, res, text ? JSON.parse(text) : undefined)); + } + }, + ); + server.maxRequestsPerSocket = 2; + server.keepAliveTimeout = 35000; + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("test address"); + return { + identity, + client: (signal?: AbortSignal) => + makeResourceWorkerClient({ ...options(tls, address.port, identity), signal }), + counts: () => ({ rpcs, identities, bytes, sockets: sockets.size }), + async close() { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + }, + }; +} +function success(identity: WorkerIdentity, request: WorkerRequest) { + return { + ok: true, + identity, + intent: request.intent, + intentId: request.intent.id, + requestDigest: request.intent.requestDigest, + operationDigest: request.intent.operationDigest, + leaseId: request.intent.lease.id, + result: true, + }; +} + +test("actual worker accepts execution-scoped disposable schemas on the pinned handshake channel", async () => { + const fixture = await actualWorker(); + const client = makeResourceWorkerClient(options(fixture.tls, fixture.port, fixture.identity)); + try { + expect(await client.identity()).toEqual(fixture.identity); + // The broker repeats the real identity check before acquiring a credential. + expect(await client.identity()).toEqual(fixture.identity); + const request = envelope(fixture.identity, schema); + const response = await client.dispatch(request); + expect(response.ok && response.result).toBe(true); + expect(response.ok && response.intent).toEqual(request.intent); + await expect(client.dispatch(request)).rejects.toThrow("MCP worker"); + expect((await fixture.command("stats")).providerDials).toBe(0); + } finally { + client.close(); + await fixture.close(); + } +}, 15000); + +test("a closed actual worker channel cannot send credentials to a replacement process", async () => { + const fixture = await actualWorker(); + const client = makeResourceWorkerClient(options(fixture.tls, fixture.port, fixture.identity)); + try { + await client.identity(); + await fixture.command("replace"); + const operation: WorkerOperation = { + kind: "execute", + endpoint: "https://tools.example/mcp", + tool: "write", + arguments: {}, + inputSchema: {}, + argumentConstraintDocuments: [], + classification: "write", + credential: { header: "authorization", value: "Bearer transient-provider-value" }, + }; + await expect(client.dispatch(envelope(fixture.identity, operation))).rejects.toThrow( + "MCP worker", + ); + expect(await fixture.command("stats")).toEqual({ + providerDials: 0, + replacementBytes: 0, + replacementRequests: 0, + }); + } finally { + client.close(); + await fixture.close(); + } +}, 15000); + +test("an actual worker restart changes boot and requires a new operation client", async () => { + const fixture = await actualWorker(); + const oldClient = makeResourceWorkerClient(options(fixture.tls, fixture.port, fixture.identity)); + const nextClient = makeResourceWorkerClient(options(fixture.tls, fixture.port, fixture.identity)); + try { + const oldIdentity = await oldClient.identity(); + const restarted: { identity: WorkerIdentity } = await fixture.command("restart"); + expect(restarted.identity.bootId).not.toBe(oldIdentity.bootId); + await expect(oldClient.dispatch(envelope(oldIdentity, schema))).rejects.toThrow("MCP worker"); + expect(await nextClient.identity()).toEqual(restarted.identity); + const response = await nextClient.dispatch(envelope(restarted.identity, schema)); + expect(response.ok && response.result).toBe(true); + expect((await fixture.command("stats")).providerDials).toBe(0); + } finally { + oldClient.close(); + nextClient.close(); + await fixture.close(); + } +}, 15000); + +test("actual schema children keep independent roots, reject coercion and do not dial providers", async () => { + const fixture = await actualWorker(); + try { + const documents = [ + { + $schema: "http://json-schema.org/draft-07/schema#", + $id: "https://schema.example/constraint", + definitions: { value: { type: "integer", maximum: 5 } }, + properties: { n: { $ref: "#/definitions/value" } }, + required: ["n"], + }, + { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: "https://schema.example/constraint", + $defs: { value: { type: "integer", minimum: 2 } }, + properties: { n: { $ref: "#/$defs/value" } }, + }, + ]; + for (const [value, expected] of [ + [{ n: 3 }, true], + [{ n: 1 }, false], + [{ n: 6 }, false], + [{ n: "3" }, false], + ] as const) { + const client = makeResourceWorkerClient(options(fixture.tls, fixture.port, fixture.identity)); + try { + const identity = await client.identity(); + const response = await client.dispatch( + envelope(identity, { kind: "validateSchemas", documents, value }), + ); + expect(response.ok && response.result).toBe(expected); + } finally { + client.close(); + } + } + expect((await fixture.command("stats")).providerDials).toBe(0); + } finally { + await fixture.close(); + } +}, 15000); + +test("separate operation clients have separate channels and never retry or reuse an RPC", async () => { + const fixture = await protocolFixture((_req, res, request) => { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify(success(fixture.identity, request!))); + }); + const clients = [fixture.client(), fixture.client()]; + try { + const identities = await Promise.all(clients.map((client) => client.identity())); + await Promise.all( + clients.map(async (client, index) => { + const request = envelope(identities[index]!, schema); + expect((await client.dispatch(request)).ok).toBe(true); + await expect(client.dispatch(request)).rejects.toThrow("MCP worker"); + await expect(client.identity()).rejects.toThrow("MCP worker"); + }), + ); + expect(fixture.counts().rpcs).toBe(2); + expect(fixture.counts().identities).toBe(2); + expect(fixture.counts().sockets).toBe(2); + } finally { + clients.forEach((client) => client.close()); + await fixture.close(); + } +}); + +test("response identity and full intent mismatches fail with sanitized errors and one send", async () => { + const changes = [ + (response: ReturnType) => ({ + ...response, + identity: { ...response.identity, bootId: "new-boot" }, + }), + (response: ReturnType) => ({ + ...response, + identity: { ...response.identity, policyRevision: "2" }, + }), + (response: ReturnType) => ({ + ...response, + intent: { ...response.intent, scope: { ...response.intent.scope, runId: "other" } }, + }), + (response: ReturnType) => ({ + ...response, + intent: { + ...response.intent, + lease: { ...response.intent.lease, expiresAt: response.intent.lease.expiresAt + 1 }, + }, + }), + (response: ReturnType) => ({ ...response, intentId: "other" }), + (response: ReturnType) => ({ ...response, operationDigest: "other" }), + (response: ReturnType) => ({ ...response, requestDigest: "other" }), + (response: ReturnType) => ({ ...response, leaseId: "other" }), + () => ({ ok: false, error: "Bearer private-value with upstream cause" }), + ]; + for (const change of changes) { + const fixture = await protocolFixture((_req, res, request) => { + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify(change(success(fixture.identity, request!)))); + }); + const client = fixture.client(); + try { + await client.identity(); + await expect(client.dispatch(envelope(fixture.identity, schema))).rejects.toThrow( + /^MCP worker operation unavailable$/, + ); + expect(fixture.counts().rpcs).toBe(1); + } finally { + client.close(); + await fixture.close(); + } + } +}, 15000); + +test("redirect, oversized, truncated and invalid UTF-8 replies cannot trigger a retry", async () => { + for (const kind of ["redirect", "size", "truncated", "utf8"] as const) { + const fixture = await protocolFixture((_req, res) => { + if (kind === "redirect") { + res.writeHead(307, { location: "/identity" }); + res.end(); + } else if (kind === "size") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("x".repeat(2 * 1024 * 1024 + 1)); + } else if (kind === "truncated") { + res.writeHead(200, { "content-type": "application/json", "content-length": 100 }); + res.end("{"); + } else { + res.writeHead(200, { "content-type": "application/json" }); + res.end(Buffer.from([0x22, 0xff, 0x22])); + } + }); + const client = fixture.client(); + try { + await client.identity(); + const request = envelope(fixture.identity, schema); + request.intent.expiresAt = Date.now() + 100; + request.intent.lease.expiresAt = request.intent.expiresAt; + await expect(client.dispatch(request)).rejects.toThrow(/^MCP worker operation unavailable$/); + expect(fixture.counts().rpcs).toBe(1); + expect(fixture.counts().identities).toBe(1); + } finally { + client.close(); + await fixture.close(); + } + } +}, 15000); + +test("abort ends an active RPC and concurrent calls cannot replace its channel", async () => { + let entered!: () => void; + const received = new Promise((resolve) => { + entered = resolve; + }); + const fixture = await protocolFixture(() => { + entered(); + }); + const controller = new AbortController(), + client = fixture.client(controller.signal); + try { + await client.identity(); + const request = envelope(fixture.identity, schema); + const pending = client.dispatch(request); + await received; + await expect(client.identity()).rejects.toThrow("MCP worker"); + await expect(client.dispatch(request)).rejects.toThrow("MCP worker"); + controller.abort(); + await expect(pending).rejects.toThrow(/^MCP worker operation unavailable$/); + expect(fixture.counts().rpcs).toBe(1); + } finally { + client.close(); + await fixture.close(); + } +}); + +test("changed boot, expired lease, invalid operation digest and oversized request send zero RPC bodies", async () => { + const fixture = await protocolFixture(() => { + throw new Error("unexpected RPC"); + }); + try { + for (const kind of ["boot", "lease", "digest", "size", "role"] as const) { + const client = fixture.client(); + try { + await client.identity(); + const request = envelope(fixture.identity, schema); + if (kind === "boot") request.intent.bootId = "other"; + else if (kind === "lease") request.intent.lease.expiresAt = Date.now() - 1; + else if (kind === "digest") request.intent.operationDigest = "wrong"; + else if (kind === "role") { + request.operation = { kind: "metadata", endpoint: "https://tools.example/metadata" }; + request.intent.operation = "metadata"; + request.intent.destination = request.operation.endpoint; + request.intent.scope = { kind: "administration", requestId: "request", actorId: "actor" }; + request.intent.operationDigest = digest(request.operation); + } else { + request.operation = { + kind: "validateSchemas", + documents: [{ description: "x".repeat(1536 * 1024) }], + }; + request.intent.operationDigest = digest(request.operation); + } + await expect(client.dispatch(request)).rejects.toThrow("MCP worker"); + } finally { + client.close(); + } + } + expect(fixture.counts().bytes).toBe(0); + expect(fixture.counts().rpcs).toBe(0); + } finally { + await fixture.close(); + } +}); + +test("TLS trust, caller identity, server pin and zone policy fail before any RPC", async () => { + const tls = await workerTls("localhost"), + other = await workerTls("localhost"); + const identity: WorkerIdentity = { + protocol: 1, + bootId: "boot", + zoneId: "public", + policyRevision: "1", + policyDigest: "a".repeat(64), + }; + let rpcs = 0; + const server = createServer( + { ...tls, requestCert: true, rejectUnauthorized: true }, + (req, res) => { + if (req.url !== "/identity") rpcs++; + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify(identity)); + }, + ); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("test address"); + const good = options(tls, address.port, identity); + try { + for (const bad of [ + { ...good, tls: { ...good.tls, ca: other.ca } }, + { ...good, tls: { ...good.tls, cert: other.cert, key: other.key } }, + { ...good, tls: { ...good.tls, serverFingerprint: other.fingerprint } }, + { ...good, policy: { ...good.policy, zoneId: "other" } }, + { ...good, policy: { ...good.policy, policyRevision: "2" } }, + { ...good, policy: { ...good.policy, policyDigest: `sha256:${"b".repeat(64)}` } }, + ]) { + const client = makeResourceWorkerClient(bad); + try { + await expect(client.identity()).rejects.toThrow("MCP worker"); + } finally { + client.close(); + } + } + expect(() => makeResourceWorkerClient({ ...good, tls: { ...good.tls, key: "" } })).toThrow(); + expect(() => + makeResourceWorkerClient({ + ...good, + tls: { ...good.tls, url: good.tls.url.replace("https:", "http:") }, + }), + ).toThrow(); + for (const url of [ + "not a URL", + `${good.tls.url}/proxy`, + `${good.tls.url}?target=other`, + good.tls.url.replace("https://", "https://user:password@"), + ]) { + expect(() => makeResourceWorkerClient({ ...good, tls: { ...good.tls, url } })).toThrow( + /^MCP worker requires pinned mTLS and policy$/, + ); + } + expect(rpcs).toBe(0); + } finally { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + } +}, 15000); + +test("server certificate pin cannot replace hostname verification", async () => { + const tls = await workerTls(); // Valid, pinned certificate for tools.example, not 127.0.0.1. + const identity: WorkerIdentity = { + protocol: 1, + bootId: "boot", + zoneId: "public", + policyRevision: "1", + policyDigest: "a".repeat(64), + }; + let requests = 0; + const server = createServer( + { ...tls, requestCert: true, rejectUnauthorized: true }, + (_req, res) => { + requests++; + res.writeHead(200, { "content-type": "application/json" }); + res.end(JSON.stringify(identity)); + }, + ); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("test address"); + const client = makeResourceWorkerClient(options(tls, address.port, identity)); + try { + await expect(client.identity()).rejects.toThrow("MCP worker"); + expect(requests).toBe(0); + } finally { + client.close(); + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + } +}); diff --git a/orchestrator/src/__tests__/mcp-worker-encoding.test.ts b/orchestrator/src/__tests__/mcp-worker-encoding.test.ts new file mode 100644 index 000000000..822e8239d --- /dev/null +++ b/orchestrator/src/__tests__/mcp-worker-encoding.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from "bun:test"; +import { toWorkerBindingHash, assertWorkerPolicy } from "../mcp/worker-encoding.ts"; + +test("worker digests use strict representation conversion without changing frozen identity", () => { + const raw = "a".repeat(64); + const frozen = { zoneId: "public", policyRevision: "1", policyDigest: `sha256:${raw}` }; + const identity = { protocol: 1 as const, bootId: "boot", ...frozen, policyDigest: raw }; + expect(toWorkerBindingHash(`sha256:${raw}`)).toBe(raw); + expect(() => assertWorkerPolicy(identity, frozen)).not.toThrow(); + expect(frozen.policyDigest).toBe(`sha256:${raw}`); + expect(identity.policyDigest).toBe(raw); + for (const value of [ + raw, + `sha256:sha256:${raw}`, + `SHA256:${raw}`, + `sha256:${raw.toUpperCase()}`, + `sha256:${raw} `, + `sha256:${raw.slice(1)}`, + ]) { + expect(() => toWorkerBindingHash(value)).toThrow(); + } + for (const policyDigest of [ + `sha256:${raw}`, + `sha256:sha256:${raw}`, + raw.toUpperCase(), + "b".repeat(64), + `${raw} `, + ]) { + expect(() => assertWorkerPolicy({ ...identity, policyDigest }, frozen)).toThrow(); + } + expect(() => assertWorkerPolicy({ ...identity, zoneId: "other" }, frozen)).toThrow(); + expect(() => assertWorkerPolicy({ ...identity, policyRevision: "2" }, frozen)).toThrow(); + expect(() => assertWorkerPolicy(identity, { ...frozen, policyDigest: raw })).toThrow(); +}); diff --git a/orchestrator/src/mcp/worker-client.ts b/orchestrator/src/mcp/worker-client.ts new file mode 100644 index 000000000..2e92f99d5 --- /dev/null +++ b/orchestrator/src/mcp/worker-client.ts @@ -0,0 +1,316 @@ +import { Agent, request as httpsRequest } from "node:https"; +import { checkServerIdentity, TLSSocket } from "node:tls"; +import type { IncomingMessage } from "node:http"; +import type { + WorkerIdentity, + WorkerRequest, + WorkerResponse, +} from "../../../mcp-worker/src/contracts.ts"; +import { canonical, digest, parseWorkerRequest } from "../../../mcp-worker/src/index.ts"; +import { assertWorkerPolicy, type ExpectedWorkerPolicy } from "./worker-encoding.ts"; +import type { CredentialControlTls } from "./control-client.ts"; + +const REQUEST_BYTES = 1536 * 1024; +const RESPONSE_BYTES = 2 * 1024 * 1024; +const IDENTITY_BYTES = 4096; +const CHANNEL_MS = 35000; +const failure = () => new Error("MCP worker operation unavailable"); +function object(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} +function identityFrom(value: unknown): WorkerIdentity { + if ( + !object(value) || + value.protocol !== 1 || + typeof value.bootId !== "string" || + !value.bootId || + value.bootId.length > 8192 || + typeof value.zoneId !== "string" || + !value.zoneId || + typeof value.policyRevision !== "string" || + !value.policyRevision || + typeof value.policyDigest !== "string" || + !/^[0-9a-f]{64}$/.test(value.policyDigest) + ) + throw failure(); + return { + protocol: 1, + bootId: value.bootId, + zoneId: value.zoneId, + policyRevision: value.policyRevision, + policyDigest: value.policyDigest, + }; +} + +/** Create one client per operation. Never share this mutable channel across calls. + * An unused identity check can be replaced before credential acquisition. Dispatch is + * one-use, and cannot reconnect or move the credential body to another TLS socket. */ +export function makeResourceWorkerClient(options: { + tls: CredentialControlTls; + policy: ExpectedWorkerPolicy; + signal?: AbortSignal; +}) { + const tls = { ...options.tls }, + policy = { ...options.policy }; + const operationSignal = options.signal; + let url: URL; + try { + url = new URL(tls.url); + } catch { + throw new Error("MCP worker requires pinned mTLS and policy"); + } + if ( + url.protocol !== "https:" || + url.username || + url.password || + url.search || + url.hash || + url.pathname !== "/" || + !tls.ca || + !tls.cert || + !tls.key || + !/^(?:[0-9A-F]{2}:){31}[0-9A-F]{2}$/.test(tls.serverFingerprint) || + !policy.zoneId || + !policy.policyRevision || + !/^sha256:[0-9a-f]{64}$/.test(policy.policyDigest) + ) + throw new Error("MCP worker requires pinned mTLS and policy"); + + let agent: Agent | undefined; + let channel: { socket: TLSSocket; identity: WorkerIdentity } | undefined; + let idleTimer: ReturnType | undefined; + let state: "unused" | "handshake" | "ready" | "dispatch" | "closed" = "unused"; + function discardChannel() { + clearTimeout(idleTimer); + channel = undefined; + agent?.destroy(); + agent = undefined; + } + function close() { + state = "closed"; + discardChannel(); + } + + async function exchange( + path: "/identity" | "/rpc", + body: string | undefined, + maximumBytes: number, + deadline: number, + signal?: AbortSignal, + requiredSocket?: TLSSocket, + ): Promise<{ value: unknown; socket: TLSSocket }> { + if (!agent || signal?.aborted || deadline <= Date.now()) throw failure(); + return new Promise((resolve, reject) => { + let finished = false; + let response: IncomingMessage | undefined; + const req = httpsRequest(url, { + agent, + path, + method: body === undefined ? "GET" : "POST", + headers: { + accept: "application/json", + ...(body === undefined + ? {} + : { + "content-type": "application/json", + "content-length": Buffer.byteLength(body), + }), + }, + maxHeaderSize: 8192, + }); + const fail = () => { + if (finished) return; + finished = true; + cleanup(); + response?.destroy(); + req.destroy(); + reject(failure()); + }; + const timer = setTimeout(fail, Math.max(1, deadline - Date.now())); + const connectTimer = setTimeout(fail, Math.min(5000, Math.max(1, deadline - Date.now()))); + function cleanup() { + clearTimeout(timer); + clearTimeout(connectTimer); + signal?.removeEventListener("abort", fail); + } + signal?.addEventListener("abort", fail, { once: true }); + req.once("error", fail); + req.once("socket", (socket) => { + if ( + !(socket instanceof TLSSocket) || + (requiredSocket && (socket !== requiredSocket || socket.destroyed || !socket.authorized)) + ) { + fail(); + return; + } + if (requiredSocket) { + clearTimeout(connectTimer); + // No credential bytes are written until socket identity has been checked. + req.end(body); + } else { + socket.once("secureConnect", () => { + if ( + !socket.authorized || + socket.getPeerCertificate().fingerprint256 !== tls.serverFingerprint + ) { + fail(); + return; + } + clearTimeout(connectTimer); + req.end(); + }); + } + }); + req.once("response", (res) => { + response = res; + const socket = res.socket; + if ( + !(socket instanceof TLSSocket) || + res.statusCode !== 200 || + !/^application\/json(?:\s*;|$)/i.test(res.headers["content-type"] ?? "") || + (res.headers["content-encoding"] && res.headers["content-encoding"] !== "identity") || + Number(res.headers["content-length"] ?? 0) > maximumBytes + ) { + fail(); + return; + } + const chunks: Buffer[] = []; + let size = 0; + res.on("data", (chunk: Buffer) => { + size += chunk.length; + if (size > maximumBytes) { + fail(); + return; + } + chunks.push(chunk); + }); + res.once("error", fail); + res.once("aborted", fail); + res.once("end", () => { + if (finished) return; + try { + const value: unknown = JSON.parse( + new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)), + ); + finished = true; + cleanup(); + resolve({ value, socket }); + } catch { + fail(); + } + }); + }); + if (signal?.aborted) fail(); + }); + } + return { + async identity(): Promise { + if (state !== "unused" && state !== "ready") throw failure(); + discardChannel(); + state = "handshake"; + try { + agent = new Agent({ + keepAlive: true, + maxSockets: 1, + maxFreeSockets: 1, + maxCachedSessions: 0, + ca: tls.ca, + cert: tls.cert, + key: tls.key, + minVersion: "TLSv1.3", + rejectUnauthorized: true, + checkServerIdentity(host, cert) { + return ( + checkServerIdentity(host, cert) ?? + (cert.fingerprint256 === tls.serverFingerprint ? undefined : failure()) + ); + }, + }); + const response = await exchange( + "/identity", + undefined, + IDENTITY_BYTES, + Date.now() + 10000, + operationSignal, + ); + const identity = identityFrom(response.value); + assertWorkerPolicy(identity, policy); + if (response.socket.destroyed) throw failure(); + channel = { socket: response.socket, identity }; + state = "ready"; + idleTimer = setTimeout(close, CHANNEL_MS); + idleTimer.unref(); + return { ...identity }; + } catch { + close(); + throw failure(); + } + }, + async dispatch(input: WorkerRequest): Promise { + if (state !== "ready" || !channel) throw failure(); + const verified = channel; + state = "dispatch"; + clearTimeout(idleTimer); + try { + const request = parseWorkerRequest(structuredClone(input)); + const { intent, operation } = request; + const now = Date.now(); + if ( + !["execute", "discover", "validateSchemas", "validateDestination", "cancel"].includes( + operation.kind, + ) || + intent.bootId !== verified.identity.bootId || + intent.zoneId !== verified.identity.zoneId || + intent.policyDigest !== verified.identity.policyDigest || + intent.operationDigest !== digest(operation) || + intent.destination !== ("endpoint" in operation ? operation.endpoint : "") || + intent.expiresAt <= now || + intent.expiresAt > now + 300000 || + intent.lease.expiresAt < intent.expiresAt || + intent.lease.expiresAt > now + 300000 || + verified.socket.destroyed + ) + throw failure(); + const body = JSON.stringify(request); + if (Buffer.byteLength(body) > REQUEST_BYTES) throw failure(); + const maximumMs = + operation.kind === "execute" ? 300000 : operation.kind === "cancel" ? 5000 : 10000; + const { value } = await exchange( + "/rpc", + body, + RESPONSE_BYTES, + Math.min(intent.expiresAt, now + maximumMs), + operationSignal, + verified.socket, + ); + if ( + !object(value) || + value.ok !== true || + canonical(value.identity) !== canonical(verified.identity) || + canonical(value.intent) !== canonical(intent) || + value.intentId !== intent.id || + value.requestDigest !== intent.requestDigest || + value.operationDigest !== intent.operationDigest || + value.leaseId !== intent.lease.id || + !("result" in value) + ) + throw failure(); + return { + ok: true, + identity: verified.identity, + intent, + intentId: intent.id, + requestDigest: intent.requestDigest, + operationDigest: intent.operationDigest, + leaseId: intent.lease.id, + result: value.result, + }; + } catch { + throw failure(); + } finally { + close(); + } + }, + close, + }; +} diff --git a/orchestrator/src/mcp/worker-encoding.ts b/orchestrator/src/mcp/worker-encoding.ts new file mode 100644 index 000000000..207d1e5af --- /dev/null +++ b/orchestrator/src/mcp/worker-encoding.ts @@ -0,0 +1,27 @@ +import type { WorkerIdentity } from "../../../mcp-worker/src/contracts.ts"; + +/** The journal keeps its prefixed digest. Only the worker scope uses bare hex. */ +export function toWorkerBindingHash(value: string): string { + if (!/^sha256:[0-9a-f]{64}$/.test(value)) throw new Error("MCP binding digest invalid"); + return value.slice("sha256:".length); +} + +export interface ExpectedWorkerPolicy { + zoneId: string; + policyRevision: string; + /** Exact frozen operator identity, in the binding's prefixed representation. */ + policyDigest: string; +} + +/** Compare representations, without changing the worker or coordinator receipt. */ +export function assertWorkerPolicy(identity: WorkerIdentity, expected: ExpectedWorkerPolicy): void { + if ( + identity.protocol !== 1 || + !/^[0-9a-f]{64}$/.test(identity.policyDigest) || + !/^sha256:[0-9a-f]{64}$/.test(expected.policyDigest) || + `sha256:${identity.policyDigest}` !== expected.policyDigest || + identity.zoneId !== expected.zoneId || + identity.policyRevision !== expected.policyRevision + ) + throw new Error("MCP worker policy changed"); +} From a197f964992db88c0a70e93b7f64da6c9e1e9a0a Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 04:11:31 -0500 Subject: [PATCH 022/279] Add MCP execution authority producers --- crates/engram-coordinator/src/mcp/policy.rs | 54 +- .../engram-coordinator/tests/mcp_control.rs | 70 + crates/engram-core/src/traits/metadata.rs | 9 + crates/engram-core/src/types/mcp_policy.rs | 43 +- crates/engram-postgres/src/lib.rs | 10 +- crates/engram-postgres/src/mcp_policy.rs | 10 +- .../proto/engram/app/v1/profile.proto | 6 + .../proto/engram/mcp/v1/session_policy.proto | 19 + crates/engram-sim/src/meta/mcp_policy.rs | 10 +- crates/engram-sim/src/meta/store_impl.rs | 10 +- crates/engram-sim/tests/meta_conformance.rs | 179 +- .../drizzle/0108_mcp_execution_authority.sql | 7 + .../0109_mcp_fixed_execute_deadline.sql | 2 + orchestrator/drizzle/meta/0108_snapshot.json | 8504 ++++++++++++++++ orchestrator/drizzle/meta/0109_snapshot.json | 8514 +++++++++++++++++ orchestrator/drizzle/meta/_journal.json | 14 + .../__tests__/mcp-phased-admission.test.ts | 197 + .../__tests__/mcp-profile-publication.test.ts | 182 + .../src/__tests__/mcp-review-fixture.ts | 46 +- .../src/__tests__/mcp-review-rpc.test.ts | 2 +- .../src/__tests__/mcp-review-store.test.ts | 10 +- orchestrator/src/__tests__/mcp-rpc.test.ts | 3 + .../__tests__/mcp-runtime-authority.test.ts | 238 + .../__tests__/mcp-session-bindings.test.ts | 30 + orchestrator/src/__tests__/mcp-store.test.ts | 47 +- orchestrator/src/__tests__/profiles.test.ts | 19 + orchestrator/src/db/profiles.ts | 129 +- orchestrator/src/db/schema.ts | 15 +- .../src/gen/engram/app/v1/profile_pb.ts | 23 +- .../gen/engram/mcp/v1/session_policy_pb.ts | 99 +- orchestrator/src/mcp/admission.ts | 149 + orchestrator/src/mcp/binding-store.ts | 3 +- orchestrator/src/mcp/journal-review.ts | 19 +- orchestrator/src/mcp/journal.ts | 178 +- orchestrator/src/mcp/runtime-authority.ts | 88 + orchestrator/src/mcp/session-bindings.ts | 16 +- orchestrator/src/mcp/session-policy.ts | 20 +- orchestrator/src/mcp/session-resolver.ts | 4 +- orchestrator/src/rpc/profiles.ts | 129 +- web/src/gen/engram/app/v1/profile_pb.ts | 23 +- ...olicy-SessionPolicyService_connectquery.ts | 5 + .../gen/engram/mcp/v1/session_policy_pb.ts | 99 +- 42 files changed, 19015 insertions(+), 219 deletions(-) create mode 100644 orchestrator/drizzle/0108_mcp_execution_authority.sql create mode 100644 orchestrator/drizzle/0109_mcp_fixed_execute_deadline.sql create mode 100644 orchestrator/drizzle/meta/0108_snapshot.json create mode 100644 orchestrator/drizzle/meta/0109_snapshot.json create mode 100644 orchestrator/src/__tests__/mcp-phased-admission.test.ts create mode 100644 orchestrator/src/__tests__/mcp-profile-publication.test.ts create mode 100644 orchestrator/src/__tests__/mcp-runtime-authority.test.ts create mode 100644 orchestrator/src/mcp/admission.ts create mode 100644 orchestrator/src/mcp/runtime-authority.ts diff --git a/crates/engram-coordinator/src/mcp/policy.rs b/crates/engram-coordinator/src/mcp/policy.rs index eae920d04..c2bdb7572 100644 --- a/crates/engram-coordinator/src/mcp/policy.rs +++ b/crates/engram-coordinator/src/mcp/policy.rs @@ -241,7 +241,13 @@ impl SessionPolicyService for PolicyControl { .authority .manager .meta - .validate_mcp_dispatch_fence(&ctx) + .validate_mcp_dispatch_fence( + &ctx, + &ExecutionRequest { + model_tool_name: r.model_tool_name, + original_arguments: r.original_arguments, + }, + ) .await .map_err(err)?; Ok(Response::new(wire::ValidateDispatchFenceResponse { @@ -254,6 +260,52 @@ impl SessionPolicyService for PolicyControl { .into(), })) } + async fn validate_execution_identity( + &self, + req: Request, + ) -> Result, Status> { + let _admission = self.control.check(&req, req.get_ref().protocol)?; + self.control + .authority + .enabled() + .map_err(|_| Status::unavailable("policy unavailable"))?; + let r = req.into_inner(); + let ctx = RequestPolicyContext { + session_id: id(&r.session_id)?, + run_id: r.run_id, + tool_call_id: r.tool_call_id, + accepted_policy_ref: from_ref(required(r.accepted_policy_ref)?)?, + accepted_write_eligibility: eligibility(&r.accepted_write_eligibility)?, + ancestor_policy_refs: r + .ancestor_policy_refs + .into_iter() + .map(from_ref) + .collect::>()?, + }; + let answer = self + .control + .authority + .manager + .meta + .validate_mcp_execution_identity( + &ctx, + &ExecutionRequest { + model_tool_name: r.model_tool_name, + original_arguments: r.original_arguments, + }, + ) + .await + .map_err(err)?; + Ok(Response::new(wire::ValidateExecutionIdentityResponse { + protocol: wire::PROTOCOL, + status: match answer { + DispatchFence::Current => "current", + DispatchFence::Stale => "stale", + DispatchFence::Unknown => "unknown", + } + .into(), + })) + } } #[cfg(test)] mod tests { diff --git a/crates/engram-coordinator/tests/mcp_control.rs b/crates/engram-coordinator/tests/mcp_control.rs index c58f6cd16..a7ef3ad29 100644 --- a/crates/engram-coordinator/tests/mcp_control.rs +++ b/crates/engram-coordinator/tests/mcp_control.rs @@ -249,6 +249,74 @@ async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() .unwrap() .unwrap(); assert_eq!(outbox.payload["result_json"], "{\"decision\":\"approve\"}"); + for state in [ + engram_core::SessionState::Pending, + engram_core::SessionState::Created, + engram_core::SessionState::Active, + ] { + meta.transition_session(policy_sid, state, engram_core::BindingDisposition::Retain) + .await + .unwrap(); + } + meta.append_session_event( + policy_sid, + "run_started", + serde_json::json!({"run_id":"run-a"}), + ) + .await + .unwrap(); + meta.append_session_event(policy_sid, "tool_call_requested", serde_json::json!({"name":"mcp_docs_search", "tool_call_id":"read-a", "run_id":"run-a", "args_json":"{ \"x\": 1 }"})).await.unwrap(); + let exact = wire::ValidateExecutionIdentityRequest { + protocol: 1, + session_id: policy_sid.to_string(), + run_id: "run-a".into(), + tool_call_id: "read-a".into(), + accepted_policy_ref: approved.r#ref.clone(), + accepted_write_eligibility: "write_eligible".into(), + ancestor_policy_refs: Vec::new(), + model_tool_name: "mcp_docs_search".into(), + original_arguments: "{ \"x\": 1 }".into(), + }; + assert_eq!( + policy_client + .validate_execution_identity(exact.clone()) + .await + .unwrap() + .into_inner() + .status, + "current" + ); + assert_eq!( + policy_client + .validate_execution_identity(wire::ValidateExecutionIdentityRequest { + original_arguments: "{\"x\":1}".into(), + ..exact.clone() + }) + .await + .unwrap() + .into_inner() + .status, + "stale" + ); + assert_eq!( + policy_client + .validate_dispatch_fence(wire::ValidateDispatchFenceRequest { + protocol: exact.protocol, + session_id: exact.session_id, + run_id: exact.run_id, + tool_call_id: exact.tool_call_id, + accepted_policy_ref: exact.accepted_policy_ref, + accepted_write_eligibility: exact.accepted_write_eligibility, + ancestor_policy_refs: exact.ancestor_policy_refs, + model_tool_name: "wrong-name".into(), + original_arguments: exact.original_arguments, + }) + .await + .unwrap() + .into_inner() + .status, + "stale" + ); let prior = policy_client .validate_dispatch_fence(wire::ValidateDispatchFenceRequest { protocol: 1, @@ -258,6 +326,8 @@ async fn control_listener_requires_tls_client_certificate_pin_and_protocol_one() accepted_policy_ref: reviewed.policy_ref, accepted_write_eligibility: "read_only".into(), ancestor_policy_refs: Vec::new(), + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into(), }) .await .unwrap() diff --git a/crates/engram-core/src/traits/metadata.rs b/crates/engram-core/src/traits/metadata.rs index aa1794da6..40b30fb6a 100644 --- a/crates/engram-core/src/traits/metadata.rs +++ b/crates/engram-core/src/traits/metadata.rs @@ -267,6 +267,15 @@ pub trait MetadataStore: Send + Sync { async fn validate_mcp_dispatch_fence( &self, _context: &crate::types::mcp_policy::RequestPolicyContext, + _expected: &crate::types::mcp_policy::ExecutionRequest, + ) -> Result { + Err(MetaError::Conflict("policy unavailable".into())) + } + /// Exact execution identity only; the trusted caller must use the write fence for writes. + async fn validate_mcp_execution_identity( + &self, + _context: &crate::types::mcp_policy::RequestPolicyContext, + _expected: &crate::types::mcp_policy::ExecutionRequest, ) -> Result { Err(MetaError::Conflict("policy unavailable".into())) } diff --git a/crates/engram-core/src/types/mcp_policy.rs b/crates/engram-core/src/types/mcp_policy.rs index 132fd18bb..80026e255 100644 --- a/crates/engram-core/src/types/mcp_policy.rs +++ b/crates/engram-core/src/types/mcp_policy.rs @@ -187,6 +187,13 @@ pub struct RequestPolicyContext { pub accepted_write_eligibility: WriteEligibility, pub ancestor_policy_refs: Vec, } +/// Exact platform request bytes, before any argument normalization. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionRequest { + pub model_tool_name: String, + pub original_arguments: String, +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DispatchFence { @@ -300,20 +307,16 @@ pub fn accept_plan( p.plan_request_digest = Some(input.review.request_digest.clone()); Ok(p) } -pub fn dispatch_fence( +pub fn execution_identity_fence( nodes: &[PolicyNode], events: &[PersistedEvent], ctx: &RequestPolicyContext, + expected: &ExecutionRequest, ) -> Result { let p = compose_policy(nodes)?; - if !nodes[0].active { + if !nodes[0].active || !p.current { return Ok(DispatchFence::Stale); } - if ctx.accepted_write_eligibility != WriteEligibility::WriteEligible - || p.write_eligibility != WriteEligibility::WriteEligible - { - return Ok(DispatchFence::Unknown); - } if ctx.session_id != ctx.accepted_policy_ref.session_id || p.reference != ctx.accepted_policy_ref || p.ancestor_policy_refs != ctx.ancestor_policy_refs @@ -331,7 +334,12 @@ pub fn dispatch_fence( "run_started" | "run_completed" | "run_interrupted" ) }); - if ctx.run_id.is_empty() + if expected.model_tool_name.is_empty() + || expected.model_tool_name.len() > 256 + || expected.original_arguments.len() > 256 * 1024 + || request.payload["name"].as_str() != Some(&expected.model_tool_name) + || request.payload["args_json"].as_str() != Some(&expected.original_arguments) + || ctx.run_id.is_empty() || request.payload["run_id"].as_str() != Some(&ctx.run_id) || last_run.is_none_or(|e| { e.kind != "run_started" || e.payload["run_id"].as_str() != Some(&ctx.run_id) @@ -341,3 +349,22 @@ pub fn dispatch_fence( } Ok(DispatchFence::Current) } + +/// Write eligibility adds to the exact execution identity checks; it never creates identity. +pub fn dispatch_fence( + nodes: &[PolicyNode], + events: &[PersistedEvent], + ctx: &RequestPolicyContext, + expected: &ExecutionRequest, +) -> Result { + let p = compose_policy(nodes)?; + if ctx.accepted_write_eligibility != WriteEligibility::WriteEligible + || p.write_eligibility != WriteEligibility::WriteEligible + || nodes + .iter() + .any(|n| !n.policy.as_ref().is_some_and(|p| p.initialized)) + { + return Ok(DispatchFence::Unknown); + } + execution_identity_fence(nodes, events, ctx, expected) +} diff --git a/crates/engram-postgres/src/lib.rs b/crates/engram-postgres/src/lib.rs index f6c33c6ad..9da01ae83 100644 --- a/crates/engram-postgres/src/lib.rs +++ b/crates/engram-postgres/src/lib.rs @@ -1538,8 +1538,16 @@ impl MetadataStore for PostgresStore { async fn validate_mcp_dispatch_fence( &self, ctx: &engram_core::types::mcp_policy::RequestPolicyContext, + expected: &engram_core::types::mcp_policy::ExecutionRequest, ) -> Result { - self.policy_fence(ctx).await + self.policy_fence(ctx, expected, true).await + } + async fn validate_mcp_execution_identity( + &self, + ctx: &engram_core::types::mcp_policy::RequestPolicyContext, + expected: &engram_core::types::mcp_policy::ExecutionRequest, + ) -> Result { + self.policy_fence(ctx, expected, false).await } async fn create_session(&self, spec: SessionSpec) -> Result { diff --git a/crates/engram-postgres/src/mcp_policy.rs b/crates/engram-postgres/src/mcp_policy.rs index c3e868d11..7720fb27f 100644 --- a/crates/engram-postgres/src/mcp_policy.rs +++ b/crates/engram-postgres/src/mcp_policy.rs @@ -109,13 +109,21 @@ impl PostgresStore { pub(super) async fn policy_fence( &self, ctx: &RequestPolicyContext, + expected: &ExecutionRequest, + write: bool, ) -> Result { let mut tx = self.pool.begin().await.map_err(db_err)?; let ns = nodes(&mut tx, ctx.session_id).await?; - let answer = dispatch_fence( + let check = if write { + dispatch_fence + } else { + execution_identity_fence + }; + let answer = check( &ns, &events(&mut tx, ctx.session_id, &ctx.tool_call_id).await?, ctx, + expected, )?; tx.commit().await.map_err(db_err)?; Ok(answer) diff --git a/crates/engram-protocol/proto/engram/app/v1/profile.proto b/crates/engram-protocol/proto/engram/app/v1/profile.proto index 04587a228..39a8ca378 100644 --- a/crates/engram-protocol/proto/engram/app/v1/profile.proto +++ b/crates/engram-protocol/proto/engram/app/v1/profile.proto @@ -84,6 +84,8 @@ message Profile { repeated SessionApp apps = 24; // When true, only automations, API keys, and admins can start sessions. bool programmatic_only = 25; + // Revision for explicit MCP grant publication; zero means no publication. + int64 mcp_grant_revision = 26; } // ADR 0118: one service a session hosts, published at @@ -231,6 +233,8 @@ message CreateProfileRequest { repeated SessionApp apps = 20; // When true, only automations, API keys, and admins can start sessions. bool programmatic_only = 21; + // Presence requests explicit MCP grant publication. First publication expects zero. + optional int64 expected_mcp_grant_revision = 22; } message CreateProfileResponse { @@ -266,6 +270,8 @@ message UpdateProfileRequest { // When true, only automations, API keys, and admins can start sessions. // Omit to preserve the current setting. optional bool programmatic_only = 22; + // Presence requests publication or republication of the exact MCP grants. + optional int64 expected_mcp_grant_revision = 23; } message UpdateProfileResponse { diff --git a/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto b/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto index 2f209ffb7..62ad9aa0c 100644 --- a/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto +++ b/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto @@ -8,6 +8,7 @@ service SessionPolicyService { rpc CreatePolicySession(CreatePolicySessionRequest) returns (CreatePolicySessionResponse); rpc ReadWritePolicy(ReadWritePolicyRequest) returns (ReadWritePolicyResponse); rpc AcceptPlanExit(AcceptPlanExitRequest) returns (AcceptPlanExitResponse); + rpc ValidateExecutionIdentity(ValidateExecutionIdentityRequest) returns (ValidateExecutionIdentityResponse); rpc ValidateDispatchFence(ValidateDispatchFenceRequest) returns (ValidateDispatchFenceResponse); } message CreatePolicySessionRequest { @@ -36,6 +37,8 @@ message ValidateDispatchFenceRequest { engram.app.v1.McpWritePolicyRef accepted_policy_ref = 5; string accepted_write_eligibility = 6; repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 7; + string model_tool_name = 8; + string original_arguments = 9; } message ValidateDispatchFenceResponse { uint32 protocol = 1; @@ -51,3 +54,19 @@ message ReadWritePolicyResponse { message AcceptPlanExitResponse { engram.app.v1.McpTrustedWritePolicy policy = 1; } + +message ValidateExecutionIdentityRequest { + uint32 protocol = 1; + string session_id = 2; + string run_id = 3; + string tool_call_id = 4; + engram.app.v1.McpWritePolicyRef accepted_policy_ref = 5; + string accepted_write_eligibility = 6; + repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 7; + string model_tool_name = 8; + string original_arguments = 9; +} +message ValidateExecutionIdentityResponse { + uint32 protocol = 1; + string status = 2; +} diff --git a/crates/engram-sim/src/meta/mcp_policy.rs b/crates/engram-sim/src/meta/mcp_policy.rs index b2b0bdad9..9288f7d71 100644 --- a/crates/engram-sim/src/meta/mcp_policy.rs +++ b/crates/engram-sim/src/meta/mcp_policy.rs @@ -70,15 +70,23 @@ impl SimMetadataStore { pub(super) fn policy_fence( &self, ctx: &RequestPolicyContext, + expected: &ExecutionRequest, + write: bool, ) -> Result { self.gate()?; let db = self.db.lock(); - dispatch_fence( + let check = if write { + dispatch_fence + } else { + execution_identity_fence + }; + check( &nodes(&db, ctx.session_id)?, db.session_events .get(&ctx.session_id) .map_or(&[], Vec::as_slice), ctx, + expected, ) } pub(super) fn policy_accept( diff --git a/crates/engram-sim/src/meta/store_impl.rs b/crates/engram-sim/src/meta/store_impl.rs index 6cfc89700..35228171c 100644 --- a/crates/engram-sim/src/meta/store_impl.rs +++ b/crates/engram-sim/src/meta/store_impl.rs @@ -731,8 +731,16 @@ impl MetadataStore for SimMetadataStore { async fn validate_mcp_dispatch_fence( &self, ctx: &engram_core::types::mcp_policy::RequestPolicyContext, + expected: &engram_core::types::mcp_policy::ExecutionRequest, ) -> Result { - self.policy_fence(ctx) + self.policy_fence(ctx, expected, true) + } + async fn validate_mcp_execution_identity( + &self, + ctx: &engram_core::types::mcp_policy::RequestPolicyContext, + expected: &engram_core::types::mcp_policy::ExecutionRequest, + ) -> Result { + self.policy_fence(ctx, expected, false) } /// `INSERT INTO sessions (id, status='pending', image_uri, mode, diff --git a/crates/engram-sim/tests/meta_conformance.rs b/crates/engram-sim/tests/meta_conformance.rs index 86fdbd2b6..925ecbe84 100644 --- a/crates/engram-sim/tests/meta_conformance.rs +++ b/crates/engram-sim/tests/meta_conformance.rs @@ -8304,7 +8304,16 @@ async fn mcp_write_policy_projections_and_fences(ctx: &Ctx) { .await .unwrap(); assert_eq!( - ctx.meta.validate_mcp_dispatch_fence(&frozen).await.unwrap(), + ctx.meta + .validate_mcp_dispatch_fence( + &frozen, + &ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into() + } + ) + .await + .unwrap(), DispatchFence::Unknown, "earlier read-only request cannot gain writes" ); @@ -8318,7 +8327,13 @@ async fn mcp_write_policy_projections_and_fences(ctx: &Ctx) { }; assert_eq!( ctx.meta - .validate_mcp_dispatch_fence(¤t) + .validate_mcp_dispatch_fence( + ¤t, + &ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into() + } + ) .await .unwrap(), DispatchFence::Current @@ -8326,19 +8341,43 @@ async fn mcp_write_policy_projections_and_fences(ctx: &Ctx) { let mut forged = current.clone(); forged.run_id = "other-run".into(); assert_eq!( - ctx.meta.validate_mcp_dispatch_fence(&forged).await.unwrap(), + ctx.meta + .validate_mcp_dispatch_fence( + &forged, + &ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into() + } + ) + .await + .unwrap(), DispatchFence::Stale ); forged = current.clone(); forged.ancestor_policy_refs.clear(); assert_eq!( - ctx.meta.validate_mcp_dispatch_fence(&forged).await.unwrap(), + ctx.meta + .validate_mcp_dispatch_fence( + &forged, + &ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into() + } + ) + .await + .unwrap(), DispatchFence::Stale ); mcp_write_policy_restrict(ctx, parent, "parent-plan").await; assert_ne!( ctx.meta - .validate_mcp_dispatch_fence(¤t) + .validate_mcp_dispatch_fence( + ¤t, + &ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into() + } + ) .await .unwrap(), DispatchFence::Current @@ -8477,7 +8516,16 @@ async fn mcp_write_policy_restore_and_generic_results(ctx: &Ctx) { ancestor_policy_refs: p.ancestor_policy_refs.clone(), }; assert_eq!( - ctx.meta.validate_mcp_dispatch_fence(&frozen).await.unwrap(), + ctx.meta + .validate_mcp_dispatch_fence( + &frozen, + &ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into() + } + ) + .await + .unwrap(), DispatchFence::Current ); ctx.meta @@ -8485,7 +8533,16 @@ async fn mcp_write_policy_restore_and_generic_results(ctx: &Ctx) { .await .unwrap(); assert_eq!( - ctx.meta.validate_mcp_dispatch_fence(&frozen).await.unwrap(), + ctx.meta + .validate_mcp_dispatch_fence( + &frozen, + &ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into() + } + ) + .await + .unwrap(), DispatchFence::Stale ); let restored = ctx.meta.read_mcp_write_policy(id).await.unwrap(); @@ -8539,8 +8596,12 @@ async fn mcp_write_policy_restriction_race(ctx: &Ctx) { accepted_write_eligibility: p.write_eligibility, ancestor_policy_refs: p.ancestor_policy_refs, }; + let expected = ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into(), + }; let (fence, ()) = tokio::join!( - ctx.meta.validate_mcp_dispatch_fence(&frozen), + ctx.meta.validate_mcp_dispatch_fence(&frozen, &expected), mcp_write_policy_restrict(ctx, id, "race-plan") ); assert!( @@ -8551,7 +8612,16 @@ async fn mcp_write_policy_restriction_race(ctx: &Ctx) { "the lock defines which operation is admitted first" ); assert_ne!( - ctx.meta.validate_mcp_dispatch_fence(&frozen).await.unwrap(), + ctx.meta + .validate_mcp_dispatch_fence( + &frozen, + &ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{}".into() + } + ) + .await + .unwrap(), DispatchFence::Current, "a committed restriction blocks every later fence" ); @@ -8650,3 +8720,94 @@ conformance!( t_mcp_write_policy_parent_lifecycle, super::mcp_write_policy_parent_lifecycle ); + +async fn mcp_read_execution_identity(ctx: &Ctx) { + use engram_core::types::mcp_policy::*; + for eligibility in [ + WriteEligibility::Unknown, + WriteEligibility::ReadOnly, + WriteEligibility::WriteEligible, + ] { + let parent = SessionId::new(); + let id = SessionId::new(); + mcp_write_policy_create(ctx, parent, None, eligibility).await; + mcp_write_policy_create(ctx, id, Some(parent), eligibility).await; + for status in [ + SessionState::Pending, + SessionState::Created, + SessionState::Active, + ] { + ctx.meta + .transition_session(id, status, BindingDisposition::Retain) + .await + .unwrap(); + } + ctx.meta + .append_session_event(id, "run_started", serde_json::json!({"run_id":"run-a"})) + .await + .unwrap(); + mcp_write_policy_event(ctx, id, "call-a", "run-a", "{ \"value\": 1 }").await; + let p = ctx.meta.read_mcp_write_policy(id).await.unwrap(); + let frozen = RequestPolicyContext { + session_id: id, + run_id: "run-a".into(), + tool_call_id: "call-a".into(), + accepted_policy_ref: p.reference, + accepted_write_eligibility: p.write_eligibility, + ancestor_policy_refs: p.ancestor_policy_refs, + }; + let expected = ExecutionRequest { + model_tool_name: "exit_plan_mode".into(), + original_arguments: "{ \"value\": 1 }".into(), + }; + assert_eq!( + ctx.meta + .validate_mcp_execution_identity(&frozen, &expected) + .await + .unwrap(), + DispatchFence::Current + ); + let mut changed = expected.clone(); + changed.original_arguments = "{\"value\":1}".into(); + assert_eq!( + ctx.meta + .validate_mcp_execution_identity(&frozen, &changed) + .await + .unwrap(), + DispatchFence::Stale + ); + changed = expected.clone(); + changed.model_tool_name = "different".into(); + assert_eq!( + ctx.meta + .validate_mcp_execution_identity(&frozen, &changed) + .await + .unwrap(), + DispatchFence::Stale + ); + let mut stale = frozen.clone(); + stale.ancestor_policy_refs.clear(); + assert_eq!( + ctx.meta + .validate_mcp_execution_identity(&stale, &expected) + .await + .unwrap(), + DispatchFence::Stale + ); + ctx.meta + .append_session_event(id, "run_interrupted", serde_json::json!({"run_id":"run-a"})) + .await + .unwrap(); + assert_eq!( + ctx.meta + .validate_mcp_execution_identity(&frozen, &expected) + .await + .unwrap(), + DispatchFence::Stale + ); + } +} +conformance!( + t_mcp_read_execution_identity, + super::mcp_read_execution_identity +); diff --git a/orchestrator/drizzle/0108_mcp_execution_authority.sql b/orchestrator/drizzle/0108_mcp_execution_authority.sql new file mode 100644 index 000000000..da8c99ee1 --- /dev/null +++ b/orchestrator/drizzle/0108_mcp_execution_authority.sql @@ -0,0 +1,7 @@ +ALTER TABLE "mcp_admission" DROP CONSTRAINT "mcp_admission_state_check";--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD COLUMN "phase" text DEFAULT 'execute' NOT NULL;--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD COLUMN "schema_queue_expires_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD COLUMN "execute_queue_expires_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD COLUMN "schema_digest" text;--> statement-breakpoint +ALTER TABLE "profile" ADD COLUMN "mcp_grant_publication" jsonb;--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD CONSTRAINT "mcp_admission_state_check" CHECK ("mcp_admission"."state" IN ('queued','admitted','released')); \ No newline at end of file diff --git a/orchestrator/drizzle/0109_mcp_fixed_execute_deadline.sql b/orchestrator/drizzle/0109_mcp_fixed_execute_deadline.sql new file mode 100644 index 000000000..b261d5aab --- /dev/null +++ b/orchestrator/drizzle/0109_mcp_fixed_execute_deadline.sql @@ -0,0 +1,2 @@ +ALTER TABLE "mcp_admission" ADD COLUMN "execute_expires_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "mcp_admission" ADD CONSTRAINT "mcp_admission_phase_check" CHECK ("mcp_admission"."phase" IN ('schema','execute')); \ No newline at end of file diff --git a/orchestrator/drizzle/meta/0108_snapshot.json b/orchestrator/drizzle/meta/0108_snapshot.json new file mode 100644 index 000000000..c71201dcd --- /dev/null +++ b/orchestrator/drizzle/meta/0108_snapshot.json @@ -0,0 +1,8504 @@ +{ + "id": "793af169-044c-4509-a33e-25a6d2a9761d", + "prevId": "66978c05-90c9-4f3c-bf04-3bfb2f7cc82c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_credential": { + "name": "mcp_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()::text" + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'execute'" + }, + "schema_queue_expires_at": { + "name": "schema_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_queue_expires_at": { + "name": "execute_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "schema_digest": { + "name": "schema_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_reservation_idx": { + "name": "mcp_admission_reservation_idx", + "columns": [ + { + "expression": "reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted','released')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_arguments": { + "name": "original_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "mcp_grant_publication": { + "name": "mcp_grant_publication", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_binding_envelope": { + "name": "mcp_binding_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/0109_snapshot.json b/orchestrator/drizzle/meta/0109_snapshot.json new file mode 100644 index 000000000..50f254347 --- /dev/null +++ b/orchestrator/drizzle/meta/0109_snapshot.json @@ -0,0 +1,8514 @@ +{ + "id": "d9212cb0-4a75-4b3f-9676-21600edc014f", + "prevId": "793af169-044c-4509-a33e-25a6d2a9761d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_credential": { + "name": "mcp_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()::text" + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'execute'" + }, + "schema_queue_expires_at": { + "name": "schema_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_queue_expires_at": { + "name": "execute_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_expires_at": { + "name": "execute_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "schema_digest": { + "name": "schema_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_reservation_idx": { + "name": "mcp_admission_reservation_idx", + "columns": [ + { + "expression": "reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_phase_check": { + "name": "mcp_admission_phase_check", + "value": "\"mcp_admission\".\"phase\" IN ('schema','execute')" + }, + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted','released')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_arguments": { + "name": "original_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "mcp_grant_publication": { + "name": "mcp_grant_publication", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_binding_envelope": { + "name": "mcp_binding_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/_journal.json b/orchestrator/drizzle/meta/_journal.json index b5498a0e4..6c5c3dabb 100644 --- a/orchestrator/drizzle/meta/_journal.json +++ b/orchestrator/drizzle/meta/_journal.json @@ -757,6 +757,20 @@ "when": 1793605822727, "tag": "0107_mcp_admission_generation", "breakpoints": true + }, + { + "idx": 108, + "version": "7", + "when": 1793609323674, + "tag": "0108_mcp_execution_authority", + "breakpoints": true + }, + { + "idx": 109, + "version": "7", + "when": 1793609726214, + "tag": "0109_mcp_fixed_execute_deadline", + "breakpoints": true } ] } \ No newline at end of file diff --git a/orchestrator/src/__tests__/mcp-phased-admission.test.ts b/orchestrator/src/__tests__/mcp-phased-admission.test.ts new file mode 100644 index 000000000..bc75208fc --- /dev/null +++ b/orchestrator/src/__tests__/mcp-phased-admission.test.ts @@ -0,0 +1,197 @@ +import { expect, test } from "bun:test"; +import { reviewFixture } from "./mcp-review-fixture.ts"; +import { canonicalDigest } from "../mcp/hash.ts"; + +// Real isolated PostgreSQL; schema success below is an explicit trusted-worker fixture. +test("schema lease is released before approval and execute gets a distinct generation", async () => { + const f = await reviewFixture(); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await expect(f.journal.requestApproval(call.id, f.deploymentId, f.now)).rejects.toThrow( + "schema", + ); + expect(await f.journal.admitSchema(call.id, f.deploymentId, f.now)).toBe("admitted"); + const schema = (await f.journal.getAdmission(call.id))!; + await f.journal.finishSchema( + call.id, + schema.reservationId, + schema.expiresAt, + call.requestDigest, + true, + f.now, + ); + expect((await f.journal.getAdmission(call.id))!.state).toBe("released"); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, f.now); + expect(approval.expiresAt.getTime()).toBe(f.now.getTime() + 600_000); + expect((await f.journal.getAdmission(call.id))!.schemaDigest).toBe(call.requestDigest); + await expect( + f.journal.finishSchema( + call.id, + schema.reservationId, + schema.expiresAt, + canonicalDigest({ wrong: true }), + true, + f.now, + ), + ).rejects.toThrow(); + } finally { + await f.cleanup(); + } +}); + +test("released failed schema preserves first queue expiry across replacement generations", async () => { + const f = await reviewFixture(); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await f.journal.admitSchema(call.id, f.deploymentId, f.now); + const first = (await f.journal.getAdmission(call.id))!; + await f.journal.finishSchema( + call.id, + first.reservationId, + first.expiresAt, + call.requestDigest, + false, + f.now, + ); + const later = new Date(f.now.getTime() + 20_000); + await f.journal.admitSchema(call.id, f.deploymentId, later); + const second = (await f.journal.getAdmission(call.id))!; + expect(second.reservationId).not.toBe(first.reservationId); + expect(second.schemaQueueExpiresAt).toEqual(first.schemaQueueExpiresAt); + await expect( + f.journal.finishSchema( + call.id, + first.reservationId, + first.expiresAt, + call.requestDigest, + true, + later, + ), + ).rejects.toThrow(); + await f.journal.finishSchema( + call.id, + second.reservationId, + second.expiresAt, + call.requestDigest, + false, + later, + ); + await expect( + f.journal.admitSchema(call.id, f.deploymentId, new Date(f.now.getTime() + 30_001)), + ).rejects.toThrow("queue"); + } finally { + await f.cleanup(); + } +}); + +test("execute replacement preserves first queue and tool expiries after approval wait", async () => { + const f = await reviewFixture(); + try { + const call = await f.journal.create( + { ...f.input, deadline: new Date(f.now.getTime() + 900_000) }, + { kind: "approval" }, + f.now, + ); + await f.journal.admitSchema(call.id, f.deploymentId, f.now); + const schema = (await f.journal.getAdmission(call.id))!; + await f.journal.finishSchema( + call.id, + schema.reservationId, + schema.expiresAt, + call.requestDigest, + true, + f.now, + ); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const { reviewContext } = await import("../mcp/journal-review.ts"); + const afterWait = new Date(f.now.getTime() + 300_000); + const approved = await f.journal.decide( + call.id, + f.actorId, + true, + reviewContext(call, approval), + afterWait, + ); + await f.journal.admit(call.id, f.deploymentId, afterWait); + const first = (await f.journal.getAdmission(call.id))!; + expect(first.reservationId).not.toBe(schema.reservationId); + expect(first.expiresAt.getTime()).toBe(afterWait.getTime() + 60_000); + expect(first.executeQueueExpiresAt!.getTime()).toBe(afterWait.getTime() + 30_000); + const proof = { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: approved.call.version - 1, + reservationId: first.reservationId, + reservationExpiresAt: first.expiresAt, + }; + await expect( + f.journal.dispatch( + call.id, + { bootId: "boot", intentId: crypto.randomUUID(), expiresAt: first.expiresAt }, + afterWait, + proof, + ), + ).rejects.toThrow("stale"); + await f.journal.admit(call.id, f.deploymentId, new Date(afterWait.getTime() + 20_000)); + const replacement = (await f.journal.getAdmission(call.id))!; + expect(replacement.reservationId).not.toBe(first.reservationId); + expect(replacement.expiresAt).toEqual(first.expiresAt); + expect(replacement.executeQueueExpiresAt).toEqual(first.executeQueueExpiresAt); + await expect( + f.journal.finishSchema( + call.id, + schema.reservationId, + schema.expiresAt, + call.requestDigest, + true, + afterWait, + ), + ).rejects.toThrow(); + expect((await f.journal.getAdmission(call.id))!.reservationId).toBe(replacement.reservationId); + } finally { + await f.cleanup(); + } +}); + +test("schema capacity is shared across database clients and released capacity promotes without renewing queue", async () => { + const fixtures = await Promise.all(Array.from({ length: 5 }, () => reviewFixture())); + const first = fixtures[0]!; + try { + const calls = await Promise.all( + fixtures.map((f) => f.journal.create(f.input, { kind: "approval" }, f.now)), + ); + const states = await Promise.all( + fixtures.map((f, i) => f.journal.admitSchema(calls[i]!.id, first.deploymentId, f.now)), + ); + expect(states.filter((state) => state === "admitted")).toHaveLength(4); + const queuedIndex = states.indexOf("queued"), + activeIndex = states.indexOf("admitted"); + const waiting = (await first.journal.getAdmission(calls[queuedIndex]!.id))!; + const active = (await first.journal.getAdmission(calls[activeIndex]!.id))!; + await first.journal.finishSchema( + calls[activeIndex]!.id, + active.reservationId, + active.expiresAt, + calls[activeIndex]!.requestDigest, + true, + first.now, + ); + expect( + await first.journal.admitSchema( + calls[queuedIndex]!.id, + first.deploymentId, + new Date(first.now.getTime() + 5_000), + ), + ).toBe("admitted"); + expect( + (await first.journal.getAdmission(calls[queuedIndex]!.id))!.schemaQueueExpiresAt, + ).toEqual(waiting.schemaQueueExpiresAt); + } finally { + const { task } = await import("../db/schema.ts"); + const { eq } = await import("drizzle-orm"); + for (const f of fixtures) await f.db.delete(task).where(eq(task.id, f.taskId)); + for (const f of fixtures) await f.cleanup(); + } +}); diff --git a/orchestrator/src/__tests__/mcp-profile-publication.test.ts b/orchestrator/src/__tests__/mcp-profile-publication.test.ts new file mode 100644 index 000000000..950239ccc --- /dev/null +++ b/orchestrator/src/__tests__/mcp-profile-publication.test.ts @@ -0,0 +1,182 @@ +import { expect, test } from "bun:test"; +import { eq } from "drizzle-orm"; +import { user, profile } from "../db/schema.ts"; +import { makeProfileStore } from "../db/profiles.ts"; +import { reviewFixture } from "./mcp-review-fixture.ts"; +import { canonicalDigest } from "../mcp/hash.ts"; + +test("MCP publication requires current admin and exact explicit revision; unrelated edits preserve provenance", async () => { + const f = await reviewFixture(); + try { + const store = makeProfileStore(f.db); + const input = (await store.get(f.profileId))!; + await expect( + store.update(f.profileId, input, { actorId: f.actorId, expectedRevision: 0 }), + ).rejects.toThrow("administrator"); + await f.db.update(user).set({ role: "admin" }).where(eq(user.id, f.actorId)); + const published = await store.update(f.profileId, input, { + actorId: f.actorId, + expectedRevision: 0, + }); + expect(published!.mcpGrantPublication).toEqual({ + revision: 1, + actorId: f.actorId, + grantsHash: canonicalDigest(input.integrationGrants), + }); + await expect( + store.update(f.profileId, input, { actorId: f.actorId, expectedRevision: 0 }), + ).rejects.toThrow("revision"); + const unchanged = await store.update(f.profileId, { ...input, name: "Changed label" }); + expect(unchanged!.mcpGrantPublication).toEqual(published!.mcpGrantPublication); + await f.db + .update(profile) + .set({ mcpGrantPublication: null }) + .where(eq(profile.id, f.profileId)); + expect( + (await store.update(f.profileId, { ...input, name: "Legacy label" }))!.mcpGrantPublication, + ).toBeNull(); + await expect(store.update(f.profileId, { ...input, integrationGrants: [] })).rejects.toThrow( + "publication", + ); + await f.db.update(user).set({ role: "user" }).where(eq(user.id, f.actorId)); + await expect( + store.update(f.profileId, input, { actorId: f.actorId, expectedRevision: 0 }), + ).rejects.toThrow("administrator"); + } finally { + await f.cleanup(); + } +}); + +test("publication waits for a current-role write and concurrent revision CAS has one winner", async () => { + const f = await reviewFixture(); + const { sql } = await import("drizzle-orm"); + try { + const store = makeProfileStore(f.db), + input = (await store.get(f.profileId))!; + await f.db.update(user).set({ role: "admin" }).where(eq(user.id, f.actorId)); + let unlock!: () => void, locked!: (pid: number) => void; + const release = new Promise((resolve) => { + unlock = resolve; + }); + const ready = new Promise((resolve) => { + locked = resolve; + }); + const mutation = f.db.transaction(async (tx) => { + await tx.update(user).set({ role: "user" }).where(eq(user.id, f.actorId)); + const rows = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`); + locked(Number(rows.rows[0]!.pid)); + await release; + }); + const blocker = await ready; + const attempt = store + .update(f.profileId, input, { actorId: f.actorId, expectedRevision: 0 }) + .then( + () => "allowed", + () => "denied", + ); + try { + const deadline = Date.now() + 2000; + let blocked = false; + while (Date.now() < deadline) { + const rows = await f.db.execute<{ waiting: boolean }>( + sql`SELECT EXISTS (SELECT 1 FROM pg_stat_activity WHERE ${blocker} = ANY(pg_blocking_pids(pid))) AS waiting`, + ); + if (rows.rows[0]?.waiting) { + blocked = true; + break; + } + await Bun.sleep(5); + } + expect(blocked).toBe(true); + } finally { + unlock(); + } + await mutation; + expect(await attempt).toBe("denied"); + await f.db.update(user).set({ role: "admin" }).where(eq(user.id, f.actorId)); + const results = await Promise.allSettled( + [1, 2].map(() => + store.update(f.profileId, input, { actorId: f.actorId, expectedRevision: 0 }), + ), + ); + expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(1); + expect((await store.get(f.profileId))!.mcpGrantPublication?.revision).toBe(1); + } finally { + await f.cleanup(); + } +}); + +test("stored resolver permits current unknown write policy but rejects terminal identity", async () => { + const f = await reviewFixture(); + const { createClient, createRouterTransport } = await import("@connectrpc/connect"); + const { SessionPolicyService } = await import("../gen/engram/mcp/v1/session_policy_pb.ts"); + const { makeStoredSessionBindingResolver } = await import("../mcp/binding-store.ts"); + let current = true; + const control = createClient( + SessionPolicyService, + createRouterTransport((router) => + router.service(SessionPolicyService, { + readWritePolicy: async () => ({ + state: { + policy: { + ref: f.input.requestPolicyContext.acceptedPolicyRef, + current, + initialized: false, + writeEligibility: "unknown", + }, + }, + }), + }), + ), + ); + try { + const resolver = makeStoredSessionBindingResolver( + { + control, + readOperatorPolicies: async () => f.authority.operatorPolicies, + enforceManagedDeny: async (expected) => expected, + }, + f.db, + ); + const { readLiveBindingAuthority, bindingSnapshotColumns } = + await import("../mcp/binding-store.ts"); + const { compileSessionBindings } = await import("../mcp/session-bindings.ts"); + const { taskSession } = await import("../db/schema.ts"); + const live = await readLiveBindingAuthority(f.profileId, f.authority.operatorPolicies, f.db); + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: f.profileId, integrationGrants: live.profileGrants }, + liveAuthority: live, + bindingEpoch: 0, + }); + await f.db + .update(taskSession) + .set( + bindingSnapshotColumns(snapshot, [ + { + id: f.connection.id, + alias: f.connection.alias, + provider: "mcp", + displayName: f.connection.displayName, + config: f.connection.config, + }, + ]), + ) + .where(eq(taskSession.sessionId, f.sessionId)); + await resolver.readCurrent(f.sessionId); + expect( + await resolver.resolveBoundTool({ + sessionId: f.sessionId, + modelToolName: f.input.resolvedTool.modelToolName, + }), + ).toBeDefined(); + current = false; + expect( + await resolver.resolveBoundTool({ + sessionId: f.sessionId, + modelToolName: f.input.resolvedTool.modelToolName, + }), + ).toBeUndefined(); + } finally { + await f.cleanup(); + } +}); diff --git a/orchestrator/src/__tests__/mcp-review-fixture.ts b/orchestrator/src/__tests__/mcp-review-fixture.ts index 841105124..1c149e9ca 100644 --- a/orchestrator/src/__tests__/mcp-review-fixture.ts +++ b/orchestrator/src/__tests__/mcp-review-fixture.ts @@ -17,7 +17,7 @@ export async function reviewFixture(documents: Record[] = [], t actorId = crypto.randomUUID(), taskId = crypto.randomUUID(), sessionId = crypto.randomUUID(), - deploymentId = crypto.randomUUID(); + deploymentId: string = crypto.randomUUID(); const now = new Date("2026-01-01T00:00:00Z"); const connections = makeIntegrationConnectionStore(db), repository = makeMcpRepository(db); @@ -122,6 +122,8 @@ export async function reviewFixture(documents: Record[] = [], t await db.insert(taskSession).values({ taskId, sessionId, + profileId, + integrationPrincipalId: actorId, ...bindingSnapshotColumns(snapshot, [ { id: connection.id, @@ -205,3 +207,45 @@ export async function reviewFixture(documents: Record[] = [], t }, }; } + +/** Stage fixture for tests of review/journal behavior after a successful worker preflight. + * It obtains and releases real durable leases; the boolean worker result is injected. */ +let priorSchemaPreparation: Promise = Promise.resolve(); +export async function schemaVerifiedFixture( + documents: Record[] = [], + toolCount = 1, + deploymentId?: string, +) { + const f = await reviewFixture(documents, toolCount); + if (deploymentId) f.deploymentId = deploymentId; + const base = f.journal; + return { + ...f, + journal: { + ...base, + async create(...args: Parameters) { + const call = await base.create(...args); + const ready = priorSchemaPreparation.then(async () => { + if ( + ["waiting_approval", "authorized"].includes(call.state) && + !(await base.getAdmission(call.id))?.schemaDigest + ) { + await base.admitSchema(call.id, f.deploymentId, args[2]); + const lease = (await base.getAdmission(call.id))!; + await base.finishSchema( + call.id, + lease.reservationId, + lease.expiresAt, + call.requestDigest, + true, + args[2], + ); + } + }); + priorSchemaPreparation = ready.catch(() => {}); + await ready; + return call; + }, + }, + }; +} diff --git a/orchestrator/src/__tests__/mcp-review-rpc.test.ts b/orchestrator/src/__tests__/mcp-review-rpc.test.ts index b5802b35c..1fb314f22 100644 --- a/orchestrator/src/__tests__/mcp-review-rpc.test.ts +++ b/orchestrator/src/__tests__/mcp-review-rpc.test.ts @@ -7,7 +7,7 @@ import { Hono } from "hono"; import { eq } from "drizzle-orm"; import { checkDb } from "../db/client.ts"; import { task, user, pendingToolCall, mcpApproval } from "../db/schema.ts"; -import { reviewFixture } from "./mcp-review-fixture.ts"; +import { schemaVerifiedFixture as reviewFixture } from "./mcp-review-fixture.ts"; import { buildServer } from "../server.ts"; import { IntegrationService, diff --git a/orchestrator/src/__tests__/mcp-review-store.test.ts b/orchestrator/src/__tests__/mcp-review-store.test.ts index 58ccf762f..b67ab8b41 100644 --- a/orchestrator/src/__tests__/mcp-review-store.test.ts +++ b/orchestrator/src/__tests__/mcp-review-store.test.ts @@ -35,7 +35,7 @@ test.skipIf(!reachable)( }, ); -import { reviewFixture } from "./mcp-review-fixture.ts"; +import { schemaVerifiedFixture as reviewFixture } from "./mcp-review-fixture.ts"; test.skipIf(!reachable)( "journal freezes original bytes and rejects argument hash mismatch", async () => { @@ -261,9 +261,7 @@ test.skipIf(!reachable)( authorization, ), ).rejects.toThrow("stale"); - expect( - await f.db.select().from(mcpAdmission).where(eq(mcpAdmission.executionId, call.id)), - ).toHaveLength(0); + expect((await f.journal.getAdmission(call.id))?.state).toBe("released"); const [approval] = await f.db .select() .from(mcpApproval) @@ -333,7 +331,9 @@ test.skipIf(!reachable)( originalArguments: null, requestContext: null, }); - await f.journal.requestApproval(legacyId, f.deploymentId, f.now); + await expect(f.journal.requestApproval(legacyId, f.deploymentId, f.now)).rejects.toThrow( + "schema", + ); const current = await f.journal.pending(f.sessionId, f.actorId, f.now); expect(current.map((row) => row.call.id)).not.toContain(legacyId); await expect( diff --git a/orchestrator/src/__tests__/mcp-rpc.test.ts b/orchestrator/src/__tests__/mcp-rpc.test.ts index 46050723f..4df7b22af 100644 --- a/orchestrator/src/__tests__/mcp-rpc.test.ts +++ b/orchestrator/src/__tests__/mcp-rpc.test.ts @@ -74,6 +74,9 @@ function services(): McpServices { pending: unavailable, decide: unavailable, admit: unavailable, + admitSchema: unavailable, + finishSchema: unavailable, + assertDispatchCurrent: unavailable, dispatch: unavailable, finish: unavailable, cancel: unavailable, diff --git a/orchestrator/src/__tests__/mcp-runtime-authority.test.ts b/orchestrator/src/__tests__/mcp-runtime-authority.test.ts new file mode 100644 index 000000000..ad2990444 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-runtime-authority.test.ts @@ -0,0 +1,238 @@ +import { expect, test } from "bun:test"; +import { eq } from "drizzle-orm"; +import { taskSession, user } from "../db/schema.ts"; +import { reviewFixture } from "./mcp-review-fixture.ts"; +import { reviewContext } from "../mcp/journal-review.ts"; + +async function ready() { + const f = await reviewFixture(); + await f.db + .update(taskSession) + .set({ integrationPrincipalId: f.actorId }) + .where(eq(taskSession.sessionId, f.sessionId)); + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await f.journal.admitSchema(call.id, f.deploymentId, f.now); + const schema = (await f.journal.getAdmission(call.id))!; + await f.journal.finishSchema( + call.id, + schema.reservationId, + schema.expiresAt, + call.requestDigest, + true, + f.now, + ); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const approved = await f.journal.decide( + call.id, + f.actorId, + true, + reviewContext(call, approval), + f.now, + ); + await f.journal.admit(call.id, f.deploymentId, f.now); + const lease = (await f.journal.getAdmission(call.id))!; + const proof = { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: approved.call.version, + reservationId: lease.reservationId, + reservationExpiresAt: lease.expiresAt, + }; + const intent = { bootId: "boot-a", intentId: crypto.randomUUID(), expiresAt: lease.expiresAt }; + return { ...f, call, proof, intent }; +} +test("barrier checks the current immutable principal and ban in its transaction", async () => { + const f = await ready(); + try { + await f.db.update(user).set({ banned: true }).where(eq(user.id, f.actorId)); + await expect(f.journal.dispatch(f.call.id, f.intent, f.now, f.proof)).rejects.toThrow(); + expect((await f.journal.get(f.call.id))!.state).toBe("authorized"); + } finally { + await f.cleanup(); + } +}); +test("barrier denies a principal changed after review", async () => { + const f = await ready(); + try { + await f.db + .update(taskSession) + .set({ integrationPrincipalId: "other" }) + .where(eq(taskSession.sessionId, f.sessionId)); + await expect(f.journal.dispatch(f.call.id, f.intent, f.now, f.proof)).rejects.toThrow(); + } finally { + await f.cleanup(); + } +}); + +test("successful barrier and both live assertions retain exact lease generation", async () => { + const f = await ready(); + try { + expect((await f.journal.dispatch(f.call.id, f.intent, f.now, f.proof)).state).toBe( + "dispatching", + ); + expect((await f.journal.assertDispatchCurrent(f.call.id, f.proof, f.now)).state).toBe( + "dispatching", + ); + await expect( + f.journal.assertDispatchCurrent( + f.call.id, + { ...f.proof, reservationId: "other-generation" }, + f.now, + ), + ).rejects.toThrow("stale"); + await f.db.update(user).set({ banned: true }).where(eq(user.id, f.actorId)); + await expect(f.journal.assertDispatchCurrent(f.call.id, f.proof, f.now)).rejects.toThrow(); + } finally { + await f.cleanup(); + } +}); + +test("automation requires exact live linkage and published grant; creator role is not authority", async () => { + const { automation, automationRun, automationSession, task } = await import("../db/schema.ts"); + const { makeProfileStore } = await import("../db/profiles.ts"); + const { compileSessionBindings } = await import("../mcp/session-bindings.ts"); + const { bindingSnapshotColumns } = await import("../mcp/binding-store.ts"); + for (const publish of [false, true]) { + const f = await reviewFixture(); + const automationId = crypto.randomUUID(), + runId = crypto.randomUUID(), + principalId = `automation:${automationId}`; + try { + const profiles = makeProfileStore(f.db); + if (publish) { + await f.db.update(user).set({ role: "admin" }).where(eq(user.id, f.actorId)); + await profiles.update(f.profileId, (await profiles.get(f.profileId))!, { + actorId: f.actorId, + expectedRevision: 0, + }); + await f.db.update(user).set({ role: "user" }).where(eq(user.id, f.actorId)); + } + const publication = (await profiles.get(f.profileId))!.mcpGrantPublication ?? null; + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: f.profileId, integrationGrants: f.authority.profileGrants }, + liveAuthority: { ...f.authority, profilePublication: publication }, + bindingEpoch: 0, + }); + await f.db + .update(taskSession) + .set({ + ...bindingSnapshotColumns(snapshot, [ + { + id: f.connection.id, + alias: f.connection.alias, + provider: "mcp", + displayName: f.connection.displayName, + config: f.connection.config, + }, + ]), + integrationPrincipalId: principalId, + }) + .where(eq(taskSession.sessionId, f.sessionId)); + await f.db.update(task).set({ createdByUserId: null }).where(eq(task.id, f.taskId)); + await f.db + .insert(automation) + .values({ id: automationId, name: "Test automation", createdByUserId: f.actorId }); + await f.db.insert(automationRun).values({ + id: runId, + automationId, + taskId: f.taskId, + sessionId: f.sessionId, + status: "running", + trigger: { source: "manual", receivedAt: f.now.toISOString() }, + }); + await f.db + .insert(automationSession) + .values({ sessionId: f.sessionId, runId, blockId: "start" }); + const input = { + ...f.input, + principalId, + bindingHash: snapshot.bindingHash, + resolvedTool: { + ...f.input.resolvedTool, + bindingHash: snapshot.bindingHash, + binding: snapshot.immutableBindings[0]!, + profilePublication: publication, + }, + }; + const call = await f.journal.create( + input, + { kind: "automation", grant: input.resolvedTool.effectiveGrant }, + f.now, + ); + await f.journal.admitSchema(call.id, f.deploymentId, f.now); + const schema = (await f.journal.getAdmission(call.id))!; + await f.journal.finishSchema( + call.id, + schema.reservationId, + schema.expiresAt, + call.requestDigest, + true, + f.now, + ); + await f.journal.admit(call.id, f.deploymentId, f.now); + const lease = (await f.journal.getAdmission(call.id))!; + const proof = { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId, + expectedJournalRevision: call.version, + reservationId: lease.reservationId, + reservationExpiresAt: lease.expiresAt, + }; + const intent = { bootId: "boot", intentId: crypto.randomUUID(), expiresAt: lease.expiresAt }; + if (!publish) + await expect(f.journal.dispatch(call.id, intent, f.now, proof)).rejects.toThrow( + "published", + ); + else { + expect((await f.journal.dispatch(call.id, intent, f.now, proof)).state).toBe("dispatching"); + await f.db + .update(automation) + .set({ enabled: false }) + .where(eq(automation.id, automationId)); + await expect(f.journal.assertDispatchCurrent(call.id, proof, f.now)).rejects.toThrow( + "automation", + ); + } + } finally { + await f.db.delete(automation).where(eq(automation.id, automationId)); + await f.cleanup(); + } + } +}); + +test("competing barriers commit once and losing cleanup preserves the committed lease", async () => { + const f = await ready(); + try { + const results = await Promise.allSettled([ + f.journal.dispatch(f.call.id, f.intent, f.now, f.proof), + f.journal.dispatch(f.call.id, f.intent, f.now, f.proof), + ]); + expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(1); + expect((await f.journal.getAdmission(f.call.id))!.reservationId).toBe(f.proof.reservationId); + expect((await f.journal.assertDispatchCurrent(f.call.id, f.proof, f.now)).state).toBe( + "dispatching", + ); + } finally { + await f.cleanup(); + } +}); + +test("barrier and human cancellation share a safe lock order", async () => { + const f = await ready(); + try { + const results = await Promise.allSettled([ + f.journal.dispatch(f.call.id, f.intent, f.now, f.proof), + f.journal.cancel(f.call.id, f.actorId, f.now), + ]); + expect(results[1]!.status).toBe("fulfilled"); + expect(["canceled", "outcome_unknown"]).toContain((await f.journal.get(f.call.id))!.state); + expect(await f.journal.getAdmission(f.call.id)).toBeNull(); + await expect(f.journal.assertDispatchCurrent(f.call.id, f.proof, f.now)).rejects.toThrow(); + } finally { + await f.cleanup(); + } +}); diff --git a/orchestrator/src/__tests__/mcp-session-bindings.test.ts b/orchestrator/src/__tests__/mcp-session-bindings.test.ts index 83a2b630d..88a3ebdcd 100644 --- a/orchestrator/src/__tests__/mcp-session-bindings.test.ts +++ b/orchestrator/src/__tests__/mcp-session-bindings.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; +import { canonicalDigest } from "../mcp/hash.ts"; import { reviewCatalog } from "../mcp/catalog.ts"; import { compileSessionBindings, @@ -186,3 +187,32 @@ test("merged manifest checks native collisions and the total tool and byte limit ), ).toThrow(); }); + +test("publication revision and publisher are frozen and cannot silently rebase a session", () => { + const source = bindingAuthority(); + const authority = { + ...source, + profilePublication: { + revision: 1, + actorId: "admin-a", + grantsHash: canonicalDigest(source.profileGrants), + }, + }; + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }); + expect(snapshot.profilePublication).toEqual(authority.profilePublication); + authority.profilePublication.revision = 2; + expect(checkBindingCurrent({ snapshot, liveAuthority: authority }).current).toBe(false); + expect(snapshot.profilePublication!.revision).toBe(1); + authority.profilePublication.grantsHash = canonicalDigest([]); + expect(() => + compileSessionBindings({ + launchPolicy: { profileId: authority.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }), + ).toThrow("publication"); +}); diff --git a/orchestrator/src/__tests__/mcp-store.test.ts b/orchestrator/src/__tests__/mcp-store.test.ts index a40eff3d8..26fc371d4 100644 --- a/orchestrator/src/__tests__/mcp-store.test.ts +++ b/orchestrator/src/__tests__/mcp-store.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { eq, sql } from "drizzle-orm"; import { checkDb } from "../db/client.ts"; import { mcpAdmission, mcpApproval, task, taskSession } from "../db/schema.ts"; -import { reviewFixture } from "./mcp-review-fixture.ts"; +import { schemaVerifiedFixture as reviewFixture } from "./mcp-review-fixture.ts"; import { bindingSnapshotColumns } from "../mcp/binding-store.ts"; import { reviewContext } from "../mcp/journal-review.ts"; import { type McpCallAuthorization, makeMcpJournal } from "../mcp/journal.ts"; @@ -20,8 +20,8 @@ const config = { auth: { kind: "api_key", secretRef: "mcp.service.api-key" }, }; // These adapters supply trusted producer fixtures only. They do not prove host or runtime enforcement. -async function fixture() { - const f = await reviewFixture(); +async function fixture(deploymentId?: string) { + const f = await reviewFixture([], 1, deploymentId); const base = f.journal; const input = { ...f.input, deadline: new Date(f.now.getTime() + 60_000) }; const actor = (id: string) => (id === "owner" ? f.actorId : id); @@ -173,17 +173,11 @@ describe("MCP durable authority", () => { await f.journal.finish(call.id, current.version, "outcome_unknown", null, false, f.now); const next = await f.journal.create( { ...f.input, toolCallId: "call-2" }, - { - kind: "automation", - grant: { - upstreamName: tool.upstreamName, - automationWrite: true, - argumentConstraintDocuments: [], - unrestricted: true, - }, - }, + { kind: "approval" }, f.now, ); + await f.journal.requestApproval(next.id, f.deploymentId, f.now); + await f.journal.decide(next.id, "owner", false, true, f.now); await f.journal.admit(next.id, f.deploymentId, f.now); await expect( f.journal.dispatch( @@ -224,16 +218,16 @@ describe("MCP durable authority", () => { ), ), ); - const statuses = await Promise.all( - calls.map((c) => f.journal.admit(c.id, f.deploymentId, f.now)), - ); - expect(statuses.filter((s) => s === "admitted")).toHaveLength(4); - expect(statuses.filter((s) => s === "queued")).toHaveLength(2); const pending = await f.journal.create( { ...f.input, toolCallId: "approval" }, { kind: "approval" }, f.now, ); + const statuses = await Promise.all( + calls.map((c) => f.journal.admit(c.id, f.deploymentId, f.now)), + ); + expect(statuses.filter((s) => s === "admitted")).toHaveLength(4); + expect(statuses.filter((s) => s === "queued")).toHaveLength(2); await f.journal.requestApproval(pending.id, f.deploymentId, f.now); await f.journal.expire(f.deploymentId, new Date(f.now.getTime() + 601_000)); expect((await f.journal.get(pending.id))!.state).toBe("canceled"); @@ -338,19 +332,9 @@ describe("MCP authority isolation", () => { async () => { const f = await fixture(); try { - const call = await f.journal.create( - f.input, - { - kind: "automation", - grant: { - upstreamName: tool.upstreamName, - automationWrite: true, - argumentConstraintDocuments: [], - unrestricted: true, - }, - }, - f.now, - ); + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await f.journal.requestApproval(call.id, f.deploymentId, f.now); + await f.journal.decide(call.id, "owner", false, true, f.now); await f.journal.admit(call.id, f.deploymentId, f.now); await f.journal.dispatch( call.id, @@ -430,7 +414,8 @@ test.skipIf(!reachable)("replicas share connection and deployment admission ceil }, }; try { - for (let i = 0; i < 9; i++) fixtures.push(await fixture()); + const sharedDeployment = crypto.randomUUID(); + for (let i = 0; i < 9; i++) fixtures.push(await fixture(sharedDeployment)); const first = fixtures[0]!; const allCalls = await Promise.all( fixtures.flatMap((f) => diff --git a/orchestrator/src/__tests__/profiles.test.ts b/orchestrator/src/__tests__/profiles.test.ts index 7694412ca..4b6be6b3f 100644 --- a/orchestrator/src/__tests__/profiles.test.ts +++ b/orchestrator/src/__tests__/profiles.test.ts @@ -794,3 +794,22 @@ describe("MCP profile grant contract", () => { } finally { await s.close(); } }); }); + +test("typed profile publication preserves optional revision presence and authenticates the publisher", async () => { + const store = makeFakeStore(); + const calls: unknown[] = []; + const originalCreate = store.create.bind(store); + store.create = async (input, publication) => { + calls.push(publication); + return originalCreate(input); + }; + const s = await spawn({ getSession: makeGetSession("admin-publisher", "admin"), store, images: fakeImages(["img-1"]) }); + const input = { name: "Profile", imageId: "img-1", harness: "claude" }; + try { + await s.client.createProfile(input); + await s.client.createProfile({ ...input, expectedMcpGrantRevision: 0n }); + expect(calls).toEqual([undefined, { actorId: "admin-publisher", expectedRevision: 0 }]); + await expectErr(s.client.createProfile({ ...input, expectedMcpGrantRevision: BigInt(Number.MAX_SAFE_INTEGER) + 1n }), Code.InvalidArgument); + expect(calls).toHaveLength(2); + } finally { await s.close(); } +}); diff --git a/orchestrator/src/db/profiles.ts b/orchestrator/src/db/profiles.ts index 3b409b89c..f3ed7f553 100644 --- a/orchestrator/src/db/profiles.ts +++ b/orchestrator/src/db/profiles.ts @@ -8,9 +8,12 @@ import { and, eq, inArray, isNull, ne } from "drizzle-orm"; +import { canonicalDigest } from "../mcp/hash.ts"; +import { isServiceAccountEmail } from "../rpc/api-key.ts"; import { getDb } from "./client.ts"; import { profile as profileTable, + user, DEFAULT_PROFILE_NETWORK, type ProfileApp, type ProfileNetwork, @@ -19,7 +22,10 @@ import { type ProfileIntegrationGrant, } from "./schema.ts"; +export type McpGrantPublication = { revision: number; actorId: string; grantsHash: string }; +export type PublishMcpGrants = { actorId: string; expectedRevision: number }; export interface ProfileRow { + mcpGrantPublication?: McpGrantPublication | null; id: string; name: string; description: string; @@ -82,9 +88,13 @@ export interface ProfileStore { getActive(id: string): Promise; /** Rows for the given ids (active or archived) — for snapshot enrichment. */ getByIds(ids: string[]): Promise; - create(input: ProfileInput): Promise; + create(input: ProfileInput, publication?: PublishMcpGrants): Promise; /** Returns the updated row, or null if the id is absent / archived. */ - update(id: string, input: ProfileInput): Promise; + update( + id: string, + input: ProfileInput, + publication?: PublishMcpGrants, + ): Promise; /** Idempotent soft delete (sets deleted_at). */ softDelete(id: string): Promise; } @@ -92,6 +102,7 @@ export interface ProfileStore { function toRow(r: typeof profileTable.$inferSelect): ProfileRow { return { id: r.id, + mcpGrantPublication: r.mcpGrantPublication, name: r.name, description: r.description, icon: r.icon, @@ -140,20 +151,79 @@ export function makeProfileStore(db: ReturnType = getDb()): Profil const rows = await db.select().from(profileTable).where(inArray(profileTable.id, ids)); return rows.map(toRow); }, - async create(input) { - const id = crypto.randomUUID(); - await db.insert(profileTable).values({ id, ...input }); - const row = await this.get(id); - return row!; + async create(input, publication) { + return db.transaction(async (tx) => { + await lockPublisher(tx, publication); + const provenance = publicationFor([], null, input.integrationGrants, publication); + const [row] = await tx + .insert(profileTable) + .values({ id: crypto.randomUUID(), ...input, mcpGrantPublication: provenance }) + .returning(); + return toRow(row!); + }); }, - async update(id, input) { - const existing = await this.getActive(id); - if (!existing) return null; - await db - .update(profileTable) - .set({ ...input, updatedAt: new Date() }) - .where(eq(profileTable.id, id)); - return this.get(id); + async update(id, input, publication) { + return db.transaction(async (tx) => { + await lockPublisher(tx, publication); + const [existing] = await tx + .select() + .from(profileTable) + .where(and(eq(profileTable.id, id), isNull(profileTable.deletedAt))) + .for("update"); + if (!existing) return null; + const provenance = publicationFor( + existing.integrationGrants, + existing.mcpGrantPublication, + input.integrationGrants, + publication, + ); + // Select only writable fields; a caller cannot supply publication metadata through input. + const { + name, + description, + icon, + imageId, + harness, + modelRouter, + model, + effort, + programmaticOnly, + includeUserTokens, + envVars, + skills, + integrationGrants, + network, + secrets, + repos, + apps, + } = input; + const [row] = await tx + .update(profileTable) + .set({ + name, + description, + icon, + imageId, + harness, + modelRouter, + model, + effort, + programmaticOnly, + includeUserTokens, + envVars, + skills, + integrationGrants, + network, + secrets, + repos, + apps, + mcpGrantPublication: provenance, + updatedAt: new Date(), + }) + .where(eq(profileTable.id, id)) + .returning(); + return toRow(row!); + }); }, async softDelete(id) { await db @@ -163,3 +233,32 @@ export function makeProfileStore(db: ReturnType = getDb()): Profil }, }; } + +type ProfileTx = Parameters["transaction"]>[0]>[0]; +async function lockPublisher(tx: ProfileTx, publication?: PublishMcpGrants) { + if (!publication) return; + const [actor] = await tx.select().from(user).where(eq(user.id, publication.actorId)).for("share"); + if (!actor || actor.banned || actor.role !== "admin" || isServiceAccountEmail(actor.email)) + throw new Error("current human administrator required for MCP grant publication"); +} +function publicationFor( + previous: ProfileIntegrationGrant[], + provenance: McpGrantPublication | null, + next: ProfileIntegrationGrant[], + publication?: PublishMcpGrants, +): McpGrantPublication | null { + const hash = (grants: ProfileIntegrationGrant[]) => + canonicalDigest(grants.filter((g) => g.mcp !== undefined)); + if (!publication) { + if (hash(previous) !== hash(next)) throw new Error("explicit MCP grant publication required"); + return provenance; + } + const revision = provenance?.revision ?? 0; + if ( + !Number.isSafeInteger(publication.expectedRevision) || + publication.expectedRevision !== revision || + revision >= Number.MAX_SAFE_INTEGER + ) + throw new Error("MCP grant publication revision changed"); + return { revision: revision + 1, actorId: publication.actorId, grantsHash: hash(next) }; +} diff --git a/orchestrator/src/db/schema.ts b/orchestrator/src/db/schema.ts index 04bbbc835..1cddf551d 100644 --- a/orchestrator/src/db/schema.ts +++ b/orchestrator/src/db/schema.ts @@ -1222,6 +1222,11 @@ export const profile = pgTable( // ADR 0109: structured integration authority. Unlike the retired flat // capability array, this preserves which configured identity an operation // uses and keeps its resource constraints attached. + mcpGrantPublication: jsonb("mcp_grant_publication").$type<{ + revision: number; + actorId: string; + grantsHash: string; + }>(), integrationGrants: jsonb("integration_grants") .$type() .notNull() @@ -2335,7 +2340,12 @@ export const mcpAdmission = pgTable( .notNull() .references(() => integrationConnection.id), sessionId: text("session_id").notNull(), - state: text("state").$type<"queued" | "admitted">().notNull(), + phase: text("phase").$type<"schema" | "execute">().notNull().default("execute"), + schemaQueueExpiresAt: timestamp("schema_queue_expires_at", { withTimezone: true }), + executeQueueExpiresAt: timestamp("execute_queue_expires_at", { withTimezone: true }), + executeExpiresAt: timestamp("execute_expires_at", { withTimezone: true }), + schemaDigest: text("schema_digest"), + state: text("state").$type<"queued" | "admitted" | "released">().notNull(), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), createdAt: timestamp("created_at", { withTimezone: true }).notNull(), }, @@ -2344,6 +2354,7 @@ export const mcpAdmission = pgTable( index("mcp_admission_deployment_idx").on(t.deploymentId, t.state, t.expiresAt), index("mcp_admission_connection_idx").on(t.connectionId, t.state), index("mcp_admission_session_idx").on(t.sessionId, t.state), - check("mcp_admission_state_check", sql`${t.state} IN ('queued','admitted')`), + check("mcp_admission_phase_check", sql`${t.phase} IN ('schema','execute')`), + check("mcp_admission_state_check", sql`${t.state} IN ('queued','admitted','released')`), ], ); diff --git a/orchestrator/src/gen/engram/app/v1/profile_pb.ts b/orchestrator/src/gen/engram/app/v1/profile_pb.ts index 84555fcb6..0ab89ac6d 100644 --- a/orchestrator/src/gen/engram/app/v1/profile_pb.ts +++ b/orchestrator/src/gen/engram/app/v1/profile_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/profile.proto. */ export const file_engram_app_v1_profile: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8ipQEKF1Byb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EhUKDWNvbm5lY3Rpb25faWQYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEhwKFHJlc291cmNlX2NvbnN0cmFpbnRzGAMgAygJEhgKEGNyZWRlbnRpYWxfc2NvcGUYBCABKAkSKAoDbWNwGAUgASgLMhsuZW5ncmFtLmFwcC52MS5NY3BUb29sR3JhbnQiUwoOUHJvZmlsZU5ldHdvcmsSDwoHZGVmYXVsdBgBIAEoCRITCgthbGxvd19ob3N0cxgCIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAMgAygJIm0KDVByb2ZpbGVTZWNyZXQSCwoDcmVmGAEgASgJEg8KB2Vudl92YXIYAiABKAkSDAoEbW9kZRgDIAEoCRITCgthbGxvd19ob3N0cxgEIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAUgAygJIm4KD1Byb2ZpbGVTbmFwc2hvdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGljb24YAyABKAkSEAoIYXJjaGl2ZWQYBCABKAgSEQoJaW1hZ2VfdXJpGAUgASgJEg4KBnNraWxscxgGIAMoCSJLChNMaXN0UHJvZmlsZXNSZXF1ZXN0EhgKEGluY2x1ZGVfYXJjaGl2ZWQYASABKAgSGgoSZm9yX3Nlc3Npb25fcGlja2VyGAIgASgIIkAKFExpc3RQcm9maWxlc1Jlc3BvbnNlEigKCHByb2ZpbGVzGAEgAygLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIh8KEUdldFByb2ZpbGVSZXF1ZXN0EgoKAmlkGAEgASgJIj0KEkdldFByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIrAFChRDcmVhdGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEgwKBGljb24YAyABKAkSEAoIaW1hZ2VfaWQYBCABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgFIAEoCBJCCghlbnZfdmFycxgGIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgHIAMoCRIuCgduZXR3b3JrGAkgASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAogAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDCABKAlIAIgBARISCgVtb2RlbBgNIAEoCUgBiAEBEhMKBmVmZm9ydBgOIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgRIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEiADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgTIAEoCUgDiAEBEicKBGFwcHMYFCADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASGQoRcHJvZ3JhbW1hdGljX29ubHkYFSABKAgaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSLXBQoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlKBAgJEApKBAgMEA1KBAgQEBFKBAgREBIiQAoVVXBkYXRlUHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiIgoURGVsZXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlUHJvZmlsZVJlc3BvbnNlIqIBCgxNY3BUb29sR3JhbnQSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRIYChBhdXRvbWF0aW9uX3dyaXRlGAIgASgIEhQKDHVucmVzdHJpY3RlZBgEIAEoCBIqCiJhcmd1bWVudF9jb25zdHJhaW50X2RvY3VtZW50c19qc29uGAUgAygJSgQIAxAEUhlhcmd1bWVudF9jb25zdHJhaW50c19qc29uMsEECg5Qcm9maWxlU2VydmljZRJXCgxMaXN0UHJvZmlsZXMSIi5lbmdyYW0uYXBwLnYxLkxpc3RQcm9maWxlc1JlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkxpc3RQcm9maWxlc1Jlc3BvbnNlElEKCkdldFByb2ZpbGUSIC5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5HZXRQcm9maWxlUmVzcG9uc2USWgoNQ3JlYXRlUHJvZmlsZRIjLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVByb2ZpbGVSZXNwb25zZRJaCg1VcGRhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVXBkYXRlUHJvZmlsZVJlc3BvbnNlEm8KFERpc2NvdmVyUHJvZmlsZVJlcG9zEiouZW5ncmFtLmFwcC52MS5EaXNjb3ZlclByb2ZpbGVSZXBvc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVzcG9uc2USWgoNRGVsZXRlUHJvZmlsZRIjLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkRlbGV0ZVByb2ZpbGVSZXNwb25zZWIGcHJvdG8z"); + fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEihQYKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIEhoKEm1jcF9ncmFudF9yZXZpc2lvbhgaIAEoAxouCgxFbnZWYXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfaGFybmVzc0IICgZfbW9kZWxCCQoHX2VmZm9ydEIPCg1fbW9kZWxfcm91dGVySgQIDBANSgQIDxAQSgQIExAUSgQIFBAVUgtkZXNpZ25hdGlvbiIoCgpTZXNzaW9uQXBwEgwKBG5hbWUYASABKAkSDAoEcG9ydBgCIAEoDSJxCgtQcm9maWxlUmVwbxIMCgRwYXRoGAEgASgJEhIKCnJlbW90ZV91cmwYAiABKAkSNQoGcmVtb3RlGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19yZW1vdGUiPgoRUHJvZmlsZVJlcG9SZW1vdGUSDAoEaG9zdBgBIAEoCRINCgVvd25lchgCIAEoCRIMCgRuYW1lGAMgASgJIjEKG0Rpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBISCgpwcm9maWxlX2lkGAEgASgJIlAKDkRpc2NvdmVyZWRSZXBvEgwKBHBhdGgYASABKAkSMAoHcmVtb3RlcxgCIAMoCzIfLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlbW90ZSJvChBEaXNjb3ZlcmVkUmVtb3RlEgwKBG5hbWUYASABKAkSCwoDdXJsGAIgASgJEjUKBnBhcnNlZBgDIAEoCzIgLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVJlcG9SZW1vdGVIAIgBAUIJCgdfcGFyc2VkIkwKHERpc2NvdmVyUHJvZmlsZVJlcG9zUmVzcG9uc2USLAoFcmVwb3MYASADKAsyHS5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyZWRSZXBvIqUBChdQcm9maWxlSW50ZWdyYXRpb25HcmFudBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhEKCW9wZXJhdGlvbhgCIAEoCRIcChRyZXNvdXJjZV9jb25zdHJhaW50cxgDIAMoCRIYChBjcmVkZW50aWFsX3Njb3BlGAQgASgJEigKA21jcBgFIAEoCzIbLmVuZ3JhbS5hcHAudjEuTWNwVG9vbEdyYW50IlMKDlByb2ZpbGVOZXR3b3JrEg8KB2RlZmF1bHQYASABKAkSEwoLYWxsb3dfaG9zdHMYAiADKAkSGwoTYWxsb3dfaG9zdF9wYXR0ZXJucxgDIAMoCSJtCg1Qcm9maWxlU2VjcmV0EgsKA3JlZhgBIAEoCRIPCgdlbnZfdmFyGAIgASgJEgwKBG1vZGUYAyABKAkSEwoLYWxsb3dfaG9zdHMYBCADKAkSGwoTYWxsb3dfaG9zdF9wYXR0ZXJucxgFIAMoCSJuCg9Qcm9maWxlU25hcHNob3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRpY29uGAMgASgJEhAKCGFyY2hpdmVkGAQgASgIEhEKCWltYWdlX3VyaRgFIAEoCRIOCgZza2lsbHMYBiADKAkiSwoTTGlzdFByb2ZpbGVzUmVxdWVzdBIYChBpbmNsdWRlX2FyY2hpdmVkGAEgASgIEhoKEmZvcl9zZXNzaW9uX3BpY2tlchgCIAEoCCJAChRMaXN0UHJvZmlsZXNSZXNwb25zZRIoCghwcm9maWxlcxgBIAMoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSIfChFHZXRQcm9maWxlUmVxdWVzdBIKCgJpZBgBIAEoCSI9ChJHZXRQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSL6BQoUQ3JlYXRlUHJvZmlsZVJlcXVlc3QSDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIMCgRpY29uGAMgASgJEhAKCGltYWdlX2lkGAQgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBSABKAgSQgoIZW52X3ZhcnMYBiADKAsyMC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYByADKAkSLgoHbmV0d29yaxgJIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgKIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGAwgASgJSACIAQESEgoFbW9kZWwYDSABKAlIAYgBARITCgZlZmZvcnQYDiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYESADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBIgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYEyABKAlIA4gBARInCgRhcHBzGBQgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBUgASgIEigKG2V4cGVjdGVkX21jcF9ncmFudF9yZXZpc2lvbhgWIAEoA0gEiAEBGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJCHgocX2V4cGVjdGVkX21jcF9ncmFudF9yZXZpc2lvbkoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSKhBgoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQESKAobZXhwZWN0ZWRfbWNwX2dyYW50X3JldmlzaW9uGBcgASgDSAWIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlCHgocX2V4cGVjdGVkX21jcF9ncmFudF9yZXZpc2lvbkoECAkQCkoECAwQDUoECBAQEUoECBEQEiJAChVVcGRhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSIiChREZWxldGVQcm9maWxlUmVxdWVzdBIKCgJpZBgBIAEoCSIXChVEZWxldGVQcm9maWxlUmVzcG9uc2UiogEKDE1jcFRvb2xHcmFudBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhgKEGF1dG9tYXRpb25fd3JpdGUYAiABKAgSFAoMdW5yZXN0cmljdGVkGAQgASgIEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YBSADKAlKBAgDEARSGWFyZ3VtZW50X2NvbnN0cmFpbnRzX2pzb24ywQQKDlByb2ZpbGVTZXJ2aWNlElcKDExpc3RQcm9maWxlcxIiLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBojLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USUQoKR2V0UHJvZmlsZRIgLmVuZ3JhbS5hcHAudjEuR2V0UHJvZmlsZVJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXNwb25zZRJaCg1DcmVhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlc3BvbnNlEloKDVVwZGF0ZVByb2ZpbGUSIy5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVzcG9uc2USbwoURGlzY292ZXJQcm9maWxlUmVwb3MSKi5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRJaCg1EZWxldGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5EZWxldGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlc3BvbnNlYgZwcm90bzM"); /** * An admin-curated bundle of session-launch inputs (ADR §1). @@ -169,6 +169,13 @@ export type Profile = Message<"engram.app.v1.Profile"> & { * @generated from field: bool programmatic_only = 25; */ programmaticOnly: boolean; + + /** + * Revision for explicit MCP grant publication; zero means no publication. + * + * @generated from field: int64 mcp_grant_revision = 26; + */ + mcpGrantRevision: bigint; }; /** @@ -722,6 +729,13 @@ export type CreateProfileRequest = Message<"engram.app.v1.CreateProfileRequest"> * @generated from field: bool programmatic_only = 21; */ programmaticOnly: boolean; + + /** + * Presence requests explicit MCP grant publication. First publication expects zero. + * + * @generated from field: optional int64 expected_mcp_grant_revision = 22; + */ + expectedMcpGrantRevision?: bigint; }; /** @@ -852,6 +866,13 @@ export type UpdateProfileRequest = Message<"engram.app.v1.UpdateProfileRequest"> * @generated from field: optional bool programmatic_only = 22; */ programmaticOnly?: boolean; + + /** + * Presence requests publication or republication of the exact MCP grants. + * + * @generated from field: optional int64 expected_mcp_grant_revision = 23; + */ + expectedMcpGrantRevision?: bigint; }; /** diff --git a/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts b/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts index a2c12482d..e15af2eab 100644 --- a/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts +++ b/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/session_policy.proto. */ export const file_engram_mcp_v1_session_policy: GenFile = /*@__PURE__*/ - fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIo0CChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYiQQodVmFsaWRhdGVEaXNwYXRjaEZlbmNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGc3RhdHVzGAIgASgJIlQKG0NyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRI1CgdzZXNzaW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVzcG9uc2UiUgoXUmVhZFdyaXRlUG9saWN5UmVzcG9uc2USNwoFc3RhdGUYASABKAsyKC5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVzcG9uc2UiTgoWQWNjZXB0UGxhbkV4aXRSZXNwb25zZRI0CgZwb2xpY3kYASABKAsyJC5lbmdyYW0uYXBwLnYxLk1jcFRydXN0ZWRXcml0ZVBvbGljeTK5AwoUU2Vzc2lvblBvbGljeVNlcnZpY2USbAoTQ3JlYXRlUG9saWN5U2Vzc2lvbhIpLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlcXVlc3QaKi5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRJgCg9SZWFkV3JpdGVQb2xpY3kSJS5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlcXVlc3QaJi5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlc3BvbnNlEl0KDkFjY2VwdFBsYW5FeGl0EiQuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlcXVlc3QaJS5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVzcG9uc2UScgoVVmFsaWRhdGVEaXNwYXRjaEZlbmNlEisuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZWIGcHJvdG8z", [file_engram_app_v1_session]); + fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCTK5BAoUU2Vzc2lvblBvbGljeVNlcnZpY2USbAoTQ3JlYXRlUG9saWN5U2Vzc2lvbhIpLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlcXVlc3QaKi5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRJgCg9SZWFkV3JpdGVQb2xpY3kSJS5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlcXVlc3QaJi5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlc3BvbnNlEl0KDkFjY2VwdFBsYW5FeGl0EiQuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlcXVlc3QaJS5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVzcG9uc2USfgoZVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eRIvLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlcXVlc3QaMC5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHlSZXNwb25zZRJyChVWYWxpZGF0ZURpc3BhdGNoRmVuY2USKy5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRGlzcGF0Y2hGZW5jZVJlcXVlc3QaLC5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRGlzcGF0Y2hGZW5jZVJlc3BvbnNlYgZwcm90bzM", [file_engram_app_v1_session]); /** * @generated from message engram.mcp.v1.CreatePolicySessionRequest @@ -148,6 +148,16 @@ export type ValidateDispatchFenceRequest = Message<"engram.mcp.v1.ValidateDispat * @generated from field: repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 7; */ ancestorPolicyRefs: McpWritePolicyRef[]; + + /** + * @generated from field: string model_tool_name = 8; + */ + modelToolName: string; + + /** + * @generated from field: string original_arguments = 9; + */ + originalArguments: string; }; /** @@ -230,6 +240,85 @@ export type AcceptPlanExitResponse = Message<"engram.mcp.v1.AcceptPlanExitRespon export const AcceptPlanExitResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_session_policy, 7); +/** + * @generated from message engram.mcp.v1.ValidateExecutionIdentityRequest + */ +export type ValidateExecutionIdentityRequest = Message<"engram.mcp.v1.ValidateExecutionIdentityRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 4; + */ + toolCallId: string; + + /** + * @generated from field: engram.app.v1.McpWritePolicyRef accepted_policy_ref = 5; + */ + acceptedPolicyRef?: McpWritePolicyRef; + + /** + * @generated from field: string accepted_write_eligibility = 6; + */ + acceptedWriteEligibility: string; + + /** + * @generated from field: repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 7; + */ + ancestorPolicyRefs: McpWritePolicyRef[]; + + /** + * @generated from field: string model_tool_name = 8; + */ + modelToolName: string; + + /** + * @generated from field: string original_arguments = 9; + */ + originalArguments: string; +}; + +/** + * Describes the message engram.mcp.v1.ValidateExecutionIdentityRequest. + * Use `create(ValidateExecutionIdentityRequestSchema)` to create a new message. + */ +export const ValidateExecutionIdentityRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 8); + +/** + * @generated from message engram.mcp.v1.ValidateExecutionIdentityResponse + */ +export type ValidateExecutionIdentityResponse = Message<"engram.mcp.v1.ValidateExecutionIdentityResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string status = 2; + */ + status: string; +}; + +/** + * Describes the message engram.mcp.v1.ValidateExecutionIdentityResponse. + * Use `create(ValidateExecutionIdentityResponseSchema)` to create a new message. + */ +export const ValidateExecutionIdentityResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 9); + /** * Dedicated pinned mTLS only. Create reuses the ordinary creation core. * @@ -260,6 +349,14 @@ export const SessionPolicyService: GenService<{ input: typeof AcceptPlanExitRequestSchema; output: typeof AcceptPlanExitResponseSchema; }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ValidateExecutionIdentity + */ + validateExecutionIdentity: { + methodKind: "unary"; + input: typeof ValidateExecutionIdentityRequestSchema; + output: typeof ValidateExecutionIdentityResponseSchema; + }, /** * @generated from rpc engram.mcp.v1.SessionPolicyService.ValidateDispatchFence */ diff --git a/orchestrator/src/mcp/admission.ts b/orchestrator/src/mcp/admission.ts new file mode 100644 index 000000000..c6bfeb571 --- /dev/null +++ b/orchestrator/src/mcp/admission.ts @@ -0,0 +1,149 @@ +import { and, asc, eq } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { + mcpAdmission as admission, + mcpAdmissionScope as scope, + mcpExecution as execution, +} from "../db/schema.ts"; +import { MCP_LIMITS } from "./limits.ts"; +import type { JournalTx } from "./journal-review.ts"; + +export async function lockAdmissionScope(tx: JournalTx, id: string) { + await tx.insert(scope).values({ id }).onConflictDoNothing(); + await tx.select().from(scope).where(eq(scope.id, id)).for("update"); +} +/** Both phases use the same durable capacity and retain their first queue deadline. */ +export function makeMcpAdmission(db = getDb()) { + return { + async reserve( + id: string, + deploymentId: string, + phase: "schema" | "execute", + now: Date, + toolMs = MCP_LIMITS.toolMs, + ): Promise<"queued" | "admitted"> { + if (!Number.isSafeInteger(toolMs) || toolMs < 1 || toolMs > MCP_LIMITS.maxToolMs) + throw new Error("invalid tool deadline"); + return db.transaction(async (tx) => { + await lockAdmissionScope(tx, deploymentId); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + if ( + !call || + !call.requestContext || + call.originalArguments === null || + call.deadline <= now || + (phase === "execute" + ? call.state !== "authorized" + : !["waiting_approval", "authorized"].includes(call.state)) + ) + throw new Error("call is not ready for admission"); + const [existing] = await tx.select().from(admission).where(eq(admission.executionId, id)); + if (existing && existing.deploymentId !== deploymentId) + throw new Error("admission deployment changed"); + if (phase === "execute" && existing?.schemaDigest !== call.requestDigest) + throw new Error("verified schema is required"); + if (phase === "schema" && existing?.schemaDigest) + throw new Error("schema already verified"); + if (existing?.state === "admitted") { + if (existing.phase !== phase || existing.expiresAt <= now) + throw new Error("admission lease unavailable"); + return "admitted"; + } + const queueExpiresAt = + (phase === "schema" ? existing?.schemaQueueExpiresAt : existing?.executeQueueExpiresAt) ?? + new Date(Math.min(call.deadline.getTime(), now.getTime() + MCP_LIMITS.queueMs)); + if (queueExpiresAt <= now) throw new Error("queue entry expired"); + const rows = await tx + .select() + .from(admission) + .where(eq(admission.deploymentId, deploymentId)) + .orderBy(asc(admission.createdAt)); + const active = rows.filter((row) => row.state === "admitted"); + const available = + active.length < MCP_LIMITS.deploymentConcurrency && + active.filter((row) => row.connectionId === call.connectionId).length < + MCP_LIMITS.connectionConcurrency && + active.filter((row) => row.sessionId === call.sessionId).length < + MCP_LIMITS.sessionConcurrency && + (phase !== "schema" || active.filter((row) => row.phase === "schema").length < 4); + if (!available && existing?.state === "queued") return "queued"; + if ( + !available && + rows.filter((row) => row.state === "queued").length >= MCP_LIMITS.queueSize + ) + throw new Error("MCP queue is full"); + const state = available ? "admitted" : "queued"; + const toolExpiresAt = + phase === "execute" && available + ? (existing?.executeExpiresAt ?? + new Date(Math.min(call.deadline.getTime(), now.getTime() + toolMs))) + : undefined; + if (toolExpiresAt && toolExpiresAt <= now) throw new Error("tool deadline expired"); + const values = { + phase, + state, + // Every newly admitted operation has a single-use generation, including schema retries. + reservationId: + existing?.state === "queued" ? existing.reservationId : crypto.randomUUID(), + expiresAt: available + ? (toolExpiresAt ?? new Date(Math.min(call.deadline.getTime(), now.getTime() + 10_000))) + : queueExpiresAt, + ...(toolExpiresAt ? { executeExpiresAt: toolExpiresAt } : {}), + ...(phase === "schema" + ? { schemaQueueExpiresAt: queueExpiresAt } + : { executeQueueExpiresAt: queueExpiresAt }), + } as const; + if (existing) await tx.update(admission).set(values).where(eq(admission.executionId, id)); + else + await tx + .insert(admission) + .values({ + ...values, + executionId: id, + deploymentId, + sessionId: call.sessionId, + connectionId: call.connectionId, + createdAt: now, + }); + return state; + }); + }, + /** Only a verified worker response may supply success. No public route exposes this method. */ + async finishSchema( + id: string, + reservationId: string, + expiresAt: Date, + requestDigest: string, + success: boolean, + now: Date, + ) { + const [observed] = await db.select().from(admission).where(eq(admission.executionId, id)); + if (!observed) throw new Error("schema admission missing"); + return db.transaction(async (tx) => { + await lockAdmissionScope(tx, observed.deploymentId); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + const [lease] = await tx + .select() + .from(admission) + .where(eq(admission.executionId, id)) + .for("update"); + if ( + !call || + !["waiting_approval", "authorized"].includes(call.state) || + call.requestDigest !== requestDigest || + !lease || + lease.phase !== "schema" || + lease.state !== "admitted" || + lease.reservationId !== reservationId || + lease.expiresAt.getTime() !== expiresAt.getTime() || + (success && (lease.expiresAt <= now || call.deadline <= now)) + ) + throw new Error("stale schema admission"); + await tx + .update(admission) + .set({ state: "released", schemaDigest: success ? requestDigest : null }) + .where(and(eq(admission.executionId, id), eq(admission.reservationId, reservationId))); + }); + }, + }; +} diff --git a/orchestrator/src/mcp/binding-store.ts b/orchestrator/src/mcp/binding-store.ts index 119da0ee3..77e8f5c6b 100644 --- a/orchestrator/src/mcp/binding-store.ts +++ b/orchestrator/src/mcp/binding-store.ts @@ -147,6 +147,7 @@ export async function readLiveBindingAuthority( return { profileId, profileGrants: currentProfile.integrationGrants, + profilePublication: currentProfile.mcpGrantPublication, connections, operatorPolicies: structuredClone([...operatorPolicies]), }; @@ -170,7 +171,7 @@ export function makeStoredSessionBindingResolver( const current = await policy.readForRequest({ sessionId }); const epoch = current.ref?.bindingEpoch; if ( - !current.initialized || + !current.current || epoch === undefined || epoch < 0n || epoch > BigInt(Number.MAX_SAFE_INTEGER) diff --git a/orchestrator/src/mcp/journal-review.ts b/orchestrator/src/mcp/journal-review.ts index dfb6746c3..ab675cb01 100644 --- a/orchestrator/src/mcp/journal-review.ts +++ b/orchestrator/src/mcp/journal-review.ts @@ -132,6 +132,12 @@ export async function lockHumanSession( const [actor] = await tx.select().from(user).where(eq(user.id, actorId)).for("share"); if (!actor || actor.banned || isServiceAccountEmail(actor.email)) throw new Error("human session owner or administrator required"); + const { owner, session, ancestors } = await lockSessionChain(tx, sessionId, includeAncestors); + if (actor.role !== "admin" && owner.createdByUserId !== actorId) + throw new Error("human session owner or administrator required"); + return { owner, session, actor, ancestors }; +} +export async function lockSessionChain(tx: JournalTx, sessionId: string, includeAncestors = true) { const chain: { sessionId: string; taskId: string; parentId: string | null }[] = []; let id: string | null = sessionId; const seen = new Set(); @@ -169,12 +175,9 @@ export async function lockHumanSession( } const owner = tasks.get(chain[0]!.taskId)!, session = sessions.get(sessionId)!; - if (actor.role !== "admin" && owner.createdByUserId !== actorId) - throw new Error("human session owner or administrator required"); return { owner, session, - actor, ancestors: chain .slice(1) .map((ref) => ({ session: sessions.get(ref.sessionId)!, owner: tasks.get(ref.taskId)! })), @@ -185,6 +188,7 @@ export async function assertLocalReviewCurrent( call: typeof mcpExecution.$inferSelect, session: typeof taskSession.$inferSelect, ancestors: { session: typeof taskSession.$inferSelect; owner: typeof task.$inferSelect }[], + connectionLock: "share" | "update" = "share", ) { if (!call.requestContext || call.originalArguments === null || !session.mcpBindingEnvelope) throw new Error("original request context unavailable"); @@ -196,6 +200,8 @@ export async function assertLocalReviewCurrent( const policy = requestPolicyContext(call.requestContext); if ( stored.bindingHash !== call.bindingHash || + canonicalDigest(stored.profilePublication ?? null) !== + canonicalDigest(bound.profilePublication ?? null) || BigInt(stored.bindingEpoch) !== policy.acceptedPolicyRef.bindingEpoch || canonicalDigest(stored.ancestorAuthorityRefs) !== canonicalDigest(bound.ancestorAuthorityRefs) || @@ -251,7 +257,9 @@ export async function assertLocalReviewCurrent( (snapshot) => canonicalDigest( (currentProfile.integrationGrants ?? []).filter((grant) => grant.mcp !== undefined), - ) !== snapshot.profileGrantsHash, + ) !== snapshot.profileGrantsHash || + canonicalDigest(currentProfile.mcpGrantPublication) !== + canonicalDigest(snapshot.profilePublication ?? null), ) ) throw new Error("stale MCP review profile"); @@ -262,7 +270,7 @@ export async function assertLocalReviewCurrent( .select() .from(integrationConnection) .where(eq(integrationConnection.id, id)) - .for("share"); + .for(connectionLock); for (const binding of bindings.filter((binding) => binding.connectionId === id)) { const [catalog] = await tx .select() @@ -272,6 +280,7 @@ export async function assertLocalReviewCurrent( !live || live.provider !== "mcp" || !live.enabled || + live.mcpCredential?.transition || live.revoked || live.alias !== binding.alias || canonicalDigest(live.config) !== canonicalDigest(binding.config) || diff --git a/orchestrator/src/mcp/journal.ts b/orchestrator/src/mcp/journal.ts index 3d30c38d7..925cd9916 100644 --- a/orchestrator/src/mcp/journal.ts +++ b/orchestrator/src/mcp/journal.ts @@ -1,3 +1,5 @@ +import { lockRuntimeAuthority } from "./runtime-authority.ts"; +import { makeMcpAdmission, lockAdmissionScope as lockScope } from "./admission.ts"; import { and, asc, eq, gt, inArray, isNull, lte, sql } from "drizzle-orm"; import { getDb } from "../db/client.ts"; import { @@ -7,7 +9,6 @@ import { mcpApproval as approval, mcpResolution as resolution, mcpAdmission as admission, - mcpAdmissionScope as scope, taskSession, } from "../db/schema.ts"; import { @@ -54,10 +55,6 @@ export type McpCallInput = Pick< requestPolicyContext: RequestPolicyContext; }; -async function lockScope(tx: Tx, id: string) { - await tx.insert(scope).values({ id }).onConflictDoNothing(); - await tx.select().from(scope).where(eq(scope.id, id)).for("update"); -} async function unknown(tx: Tx | Db, sessionId: string, connectionId: string): Promise { const rows = await tx .select({ id: execution.id }) @@ -88,7 +85,11 @@ export interface McpLiveDispatchAuthorization { reservationExpiresAt: Date; } export function makeMcpJournal(db = getDb()) { + const phasedAdmission = makeMcpAdmission(db); return { + admitSchema: (id: string, deploymentId: string, now: Date) => + phasedAdmission.reserve(id, deploymentId, "schema", now), + finishSchema: phasedAdmission.finishSchema, async get(id: string) { return (await db.select().from(execution).where(eq(execution.id, id)))[0] ?? null; }, @@ -190,6 +191,8 @@ export function makeMcpJournal(db = getDb()) { ); if ( snapshot.bindingHash !== input.bindingHash || + canonicalDigest(snapshot.profilePublication ?? null) !== + canonicalDigest(resolvedTool.profilePublication ?? null) || BigInt(snapshot.bindingEpoch) !== policy.acceptedPolicyRef.bindingEpoch || !snapshot.immutableBindings.some( (binding) => canonicalDigest(binding) === canonicalDigest(bound), @@ -231,6 +234,9 @@ export function makeMcpJournal(db = getDb()) { const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); if (!call || call.state !== "waiting_approval" || call.deadline <= now) throw new Error("call is not pending approval"); + const [schema] = await tx.select().from(admission).where(eq(admission.executionId, id)); + if (!schema || schema.schemaDigest !== call.requestDigest || schema.state !== "released") + throw new Error("verified released schema is required"); const [existing] = await tx.select().from(approval).where(eq(approval.executionId, id)); if (existing) return existing; const pending = await tx @@ -420,65 +426,8 @@ export function makeMcpJournal(db = getDb()) { return { call: updated!, approval: decision!, reviewedContext: expected }; }); }, - async admit( - id: string, - deploymentId: string, - now: Date, - toolMs = MCP_LIMITS.toolMs, - ): Promise<"queued" | "admitted"> { - if (!Number.isSafeInteger(toolMs) || toolMs < 1 || toolMs > MCP_LIMITS.maxToolMs) - throw new Error("invalid tool deadline"); - return db.transaction(async (tx) => { - await lockScope(tx, deploymentId); - const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); - if (!call || call.state !== "authorized" || call.deadline <= now) - throw new Error("call is not ready for admission"); - const rows = await tx - .select() - .from(admission) - .where(eq(admission.deploymentId, deploymentId)) - .orderBy(asc(admission.createdAt)); - const existing = rows.find((a) => a.executionId === id); - if (existing?.state === "admitted") return "admitted"; - if (existing && existing.expiresAt <= now) throw new Error("queue entry expired"); - // Expired active leases still count until expire() records the unknown outcome. - const active = rows.filter((a) => a.state === "admitted"); - const available = - active.length < MCP_LIMITS.deploymentConcurrency && - active.filter((a) => a.connectionId === call.connectionId).length < - MCP_LIMITS.connectionConcurrency && - active.filter((a) => a.sessionId === call.sessionId).length < - MCP_LIMITS.sessionConcurrency; - // Polling cannot extend the original queue deadline. Promotion starts a tool lease. - if (existing && !available) return "queued"; - const state = available ? "admitted" : "queued"; - if ( - !available && - !existing && - rows.filter((a) => a.state === "queued").length >= MCP_LIMITS.queueSize - ) - throw new Error("MCP queue is full"); - const expiresAt = new Date( - Math.min( - call.deadline.getTime(), - now.getTime() + (available ? toolMs : MCP_LIMITS.queueMs), - ), - ); - if (existing) - await tx.update(admission).set({ state, expiresAt }).where(eq(admission.executionId, id)); - else - await tx.insert(admission).values({ - executionId: id, - deploymentId, - sessionId: call.sessionId, - connectionId: call.connectionId, - state, - expiresAt, - createdAt: now, - }); - return state; - }); - }, + admit: (id: string, deploymentId: string, now: Date, toolMs = MCP_LIMITS.toolMs) => + phasedAdmission.reserve(id, deploymentId, "execute", now, toolMs), /** Irreversible dispatch barrier. The upstream effect runs after this commits. */ async dispatch( id: string, @@ -493,12 +442,10 @@ export function makeMcpJournal(db = getDb()) { try { return await db.transaction(async (tx) => { await lockScope(tx, reservation.deploymentId); - // Connection lock serializes unknown-outcome checks with finish/revoke. - const [identity] = await tx - .select({ connectionId: execution.connectionId }) - .from(execution) - .where(eq(execution.id, id)); + const [identity] = await tx.select().from(execution).where(eq(execution.id, id)); if (!identity) throw new Error("call not found"); + // Scope -> principal -> tasks/sessions -> profiles -> sorted connections -> call. + await lockRuntimeAuthority(tx, identity); const [live] = await tx .select() .from(connection) @@ -571,6 +518,8 @@ export function makeMcpJournal(db = getDb()) { ); if ( !lease || + lease.phase !== "execute" || + lease.schemaDigest !== call.requestDigest || lease.reservationId !== reservation.reservationId || lease.reservationId !== liveAuthorization.reservationId || lease.expiresAt.getTime() !== liveAuthorization.reservationExpiresAt.getTime() || @@ -628,7 +577,8 @@ export function makeMcpJournal(db = getDb()) { reservation.expiresAt.getTime() === liveAuthorization.reservationExpiresAt.getTime() ) await tx - .delete(admission) + .update(admission) + .set({ state: "released" }) .where( and( eq(admission.executionId, id), @@ -640,6 +590,64 @@ export function makeMcpJournal(db = getDb()) { throw error; } }, + /** Both credential-broker checks use the same committed generation and barrier revision. */ + async assertDispatchCurrent(id: string, proof: McpLiveDispatchAuthorization, now: Date) { + const [observed] = await db.select().from(admission).where(eq(admission.executionId, id)); + if (!observed) throw new Error("dispatch admission unavailable"); + return db.transaction(async (tx) => { + await lockScope(tx, observed.deploymentId); + const [identity] = await tx.select().from(execution).where(eq(execution.id, id)); + if (!identity) throw new Error("call not found"); + await lockRuntimeAuthority(tx, identity, "share"); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("share"); + const [lease] = await tx + .select() + .from(admission) + .where(eq(admission.executionId, id)) + .for("share"); + if ( + !call || + call.state !== "dispatching" || + call.deadline <= now || + !call.intentExpiresAt || + call.intentExpiresAt <= now || + proof.executionId !== id || + proof.requestDigest !== call.requestDigest || + proof.bindingHash !== call.bindingHash || + proof.principalId !== call.principalId || + proof.expectedJournalRevision + 1 !== call.version || + !lease || + lease.phase !== "execute" || + lease.state !== "admitted" || + lease.schemaDigest !== call.requestDigest || + lease.reservationId !== proof.reservationId || + lease.expiresAt.getTime() !== proof.reservationExpiresAt.getTime() || + lease.expiresAt <= now + ) + throw new Error("stale dispatch admission"); + if (call.authorizationKind === "approval") { + const [decision] = await tx + .select() + .from(approval) + .where(eq(approval.executionId, id)) + .for("share"); + if ( + !decision || + decision.decision !== "approved" || + !decision.consumedAt || + decision.requestDigest !== call.requestDigest || + decision.expiresAt <= now + ) + throw new Error("dispatch approval unavailable"); + } + if ( + call.classification === "write" && + (await unknown(tx, call.sessionId, call.connectionId)) + ) + throw new Error("unknown write outcome blocks this connection"); + return call; + }); + }, async finish( id: string, version: number, @@ -755,7 +763,7 @@ export function makeMcpJournal(db = getDb()) { and( inArray(execution.state, ["waiting_approval", "authorized"]), lte(execution.deadline, now), - isNull(admission.executionId), + sql`(${admission.executionId} IS NULL OR ${admission.state} = 'released')`, ), ) .orderBy(asc(execution.deadline)) @@ -780,7 +788,7 @@ export function makeMcpJournal(db = getDb()) { .select() .from(admission) .where(eq(admission.executionId, call.id)); - if (lease) return false; + if (lease && lease.state !== "released") return false; await tx .update(execution) .set({ state: "canceled", version: call.version + 1, updatedAt: now }) @@ -808,7 +816,13 @@ export function makeMcpJournal(db = getDb()) { const expired = await tx .select() .from(admission) - .where(and(eq(admission.deploymentId, deploymentId), lte(admission.expiresAt, now))); + .where( + and( + eq(admission.deploymentId, deploymentId), + inArray(admission.state, ["queued", "admitted"]), + lte(admission.expiresAt, now), + ), + ); for (const lease of expired.sort((a, b) => a.connectionId.localeCompare(b.connectionId))) { await tx .select() @@ -820,7 +834,18 @@ export function makeMcpJournal(db = getDb()) { .from(execution) .where(eq(execution.id, lease.executionId)) .for("update"); - if (call && ["authorized", "dispatching"].includes(call.state)) + if ( + lease.phase === "schema" && + lease.schemaQueueExpiresAt && + lease.schemaQueueExpiresAt > now + ) { + await tx + .update(admission) + .set({ state: "released" }) + .where(eq(admission.executionId, lease.executionId)); + continue; + } + if (call && ["waiting_approval", "authorized", "dispatching"].includes(call.state)) await tx .update(execution) .set({ @@ -829,7 +854,10 @@ export function makeMcpJournal(db = getDb()) { updatedAt: now, }) .where(eq(execution.id, call.id)); - await tx.delete(admission).where(eq(admission.executionId, lease.executionId)); + await tx + .update(admission) + .set({ state: "released" }) + .where(eq(admission.executionId, lease.executionId)); } const pending = await tx .select() diff --git a/orchestrator/src/mcp/runtime-authority.ts b/orchestrator/src/mcp/runtime-authority.ts new file mode 100644 index 000000000..d4b344e46 --- /dev/null +++ b/orchestrator/src/mcp/runtime-authority.ts @@ -0,0 +1,88 @@ +import { subject } from "@casl/ability"; +import { eq } from "drizzle-orm"; +import { automation, automationRun, automationSession, mcpExecution } from "../db/schema.ts"; +import { abilityFor } from "../authz/ability.ts"; +import { canonicalDigest } from "./hash.ts"; +import { + assertLocalReviewCurrent, + lockHumanSession, + lockSessionChain, + type JournalTx, +} from "./journal-review.ts"; + +/** Local authorization stays in the journal transaction. No network or cached role is accepted. */ +export async function lockRuntimeAuthority( + tx: JournalTx, + call: typeof mcpExecution.$inferSelect, + connectionLock: "share" | "update" = "update", +) { + let authority: Awaited>; + if (call.principalId.startsWith("automation:")) { + const id = call.principalId.slice("automation:".length); + const [linked] = await tx + .select() + .from(automationSession) + .where(eq(automationSession.sessionId, call.sessionId)); + if (!linked || !id) throw new Error("automation identity unavailable"); + const [owner] = await tx.select().from(automation).where(eq(automation.id, id)).for("share"); + const [run] = await tx + .select() + .from(automationRun) + .where(eq(automationRun.id, linked.runId)) + .for("share"); + const [session] = await tx + .select() + .from(automationSession) + .where(eq(automationSession.sessionId, call.sessionId)) + .for("share"); + if ( + !owner?.enabled || + owner.archivedAt || + !run || + run.automationId !== id || + run.status !== "running" || + run.dryRun || + run.endedAt || + session?.runId !== run.id || + (session.role === "primary" && + (run.sessionId !== call.sessionId || run.taskId !== call.taskId)) + ) + throw new Error("automation authority unavailable"); + authority = await lockSessionChain(tx, call.sessionId); + if (call.classification === "write") { + const resolved = call.requestContext?.resolvedTool; + const publication = authority.session.mcpBindingEnvelope?.profilePublication; + if ( + call.authorizationKind !== "automation" || + !resolved?.effectiveGrant.automationWrite || + !publication || + !Number.isSafeInteger(publication.revision) || + publication.revision < 1 || + !publication.actorId || + publication.grantsHash !== authority.session.mcpBindingEnvelope?.profileGrantsHash || + canonicalDigest(resolved.effectiveGrant) !== call.automationGrantHash + ) + throw new Error("explicit published automation write grant required"); + } + } else { + const human = await lockHumanSession(tx, call.sessionId, call.principalId, true); + const ability = abilityFor({ id: human.actor.id, role: human.actor.role ?? "user" }); + const owner = { createdByUserId: human.owner.createdByUserId }; + if ( + !ability.can("update", subject("Task", { ...owner })) || + !ability.can("prompt", subject("Session", { ...owner })) || + call.authorizationKind === "automation" + ) + throw new Error("current session permission required"); + authority = human; + } + if ( + authority.session.profileId !== authority.session.mcpBindingEnvelope?.profileId || + authority.session.integrationPrincipalId !== call.principalId || + authority.session.taskId !== call.taskId || + ["done", "failed", "cancelled"].includes(authority.owner.status) + ) + throw new Error("current session principal required"); + await assertLocalReviewCurrent(tx, call, authority.session, authority.ancestors, connectionLock); + return authority; +} diff --git a/orchestrator/src/mcp/session-bindings.ts b/orchestrator/src/mcp/session-bindings.ts index 6bc1b9c13..df584c867 100644 --- a/orchestrator/src/mcp/session-bindings.ts +++ b/orchestrator/src/mcp/session-bindings.ts @@ -17,6 +17,7 @@ export interface McpOperatorPolicy { aliases: string[]; } export interface McpLiveAuthority { + profilePublication?: import("../db/profiles.ts").McpGrantPublication | null; profileId: string; profileGrants: ProfileIntegrationGrant[]; connections: { @@ -51,6 +52,7 @@ export interface McpSessionBindingSnapshot { bindingEpoch: number; profileId: string; profileGrantsHash: string; + profilePublication?: import("../db/profiles.ts").McpGrantPublication | null; managedAuthorityHash: string; immutableBindings: McpSessionBinding[]; ancestorAuthorityRefs: McpAncestorAuthorityRef[]; @@ -167,6 +169,15 @@ export function compileSessionBindings(input: { throw new Error("MCP binding context unavailable"); if (profileHash(launchPolicy.integrationGrants) !== profileHash(live.profileGrants)) throw new Error("MCP profile changed during compilation"); + const publication = live.profilePublication; + if ( + publication && + (!Number.isSafeInteger(publication.revision) || + publication.revision < 1 || + !publication.actorId || + publication.grantsHash !== profileHash(live.profileGrants)) + ) + throw new Error("MCP grant publication identity unavailable"); let grants = mcpGrants(launchPolicy.integrationGrants); if (input.requestedGrants !== undefined) grants = intersectMcpGrants(grants, mcpGrants(input.requestedGrants)); @@ -227,6 +238,7 @@ export function compileSessionBindings(input: { bindingEpoch: input.bindingEpoch, profileId: live.profileId, profileGrantsHash: profileHash(live.profileGrants), + profilePublication: live.profilePublication ?? null, immutableBindings, ancestorAuthorityRefs, managedAuthorityHash: managedAuthorityHash(live), @@ -251,7 +263,9 @@ export function checkBindingCurrent(input: { } if ( snapshot.profileId !== live.profileId || - snapshot.profileGrantsHash !== profileHash(live.profileGrants) + snapshot.profileGrantsHash !== profileHash(live.profileGrants) || + canonicalDigest(snapshot.profilePublication ?? null) !== + canonicalDigest(live.profilePublication ?? null) ) staleReasons.push("profile_grants"); try { diff --git a/orchestrator/src/mcp/session-policy.ts b/orchestrator/src/mcp/session-policy.ts index 97abad385..c5817534f 100644 --- a/orchestrator/src/mcp/session-policy.ts +++ b/orchestrator/src/mcp/session-policy.ts @@ -162,9 +162,25 @@ export function makeSessionWritePolicy( throw new ConnectError("session policy unavailable", Code.FailedPrecondition); return response.state.policy; }, + async validateExecutionIdentity( + context: RequestPolicyContext, + expected: { modelToolName: string; originalArguments: string }, + ) { + const result = await control.validateExecutionIdentity({ + protocol: 1, + ...context, + ...expected, + }); + if (result.protocol !== 1 || !["current", "stale", "unknown"].includes(result.status)) + throw new ConnectError("session identity fence unavailable", Code.FailedPrecondition); + return result.status; + }, /** After the durable barrier, immediately before dispatch. Never cache a success. */ - async validateDispatchFence(context: RequestPolicyContext) { - const result = await control.validateDispatchFence({ protocol: 1, ...context }); + async validateDispatchFence( + context: RequestPolicyContext, + expected: { modelToolName: string; originalArguments: string }, + ) { + const result = await control.validateDispatchFence({ protocol: 1, ...context, ...expected }); if (result.protocol !== 1 || !["current", "stale", "unknown"].includes(result.status)) throw new ConnectError("session policy fence unavailable", Code.FailedPrecondition); return result.status; diff --git a/orchestrator/src/mcp/session-resolver.ts b/orchestrator/src/mcp/session-resolver.ts index 13c6a31bb..0d3e22f86 100644 --- a/orchestrator/src/mcp/session-resolver.ts +++ b/orchestrator/src/mcp/session-resolver.ts @@ -45,12 +45,13 @@ export interface BoundSessionAuthority { export interface SessionBindingResolverDeps { readSession(sessionId: string): Promise; readAuthority(profileId: string): Promise; - /** Fresh coordinator policy reference. Unknown/legacy/terminal sessions must reject. */ + /** Fresh coordinator policy reference. Missing or terminal identity must reject; write eligibility is separate. */ readBindingEpoch(sessionId: string): Promise; /** Mandatory trusted host read/apply fence. Do not implement with a stored JSON receipt. */ enforceManagedDeny?(expected: ManagedDenyAck, policy: ManagedDenyPolicy): Promise; } export interface ResolvedBoundTool { + profilePublication?: import("../db/profiles.ts").McpGrantPublication | null; bindingHash: string; binding: McpSessionBinding; modelToolName: string; @@ -115,6 +116,7 @@ export function makeSessionBindingResolver(deps: SessionBindingResolverDeps) { if (bound) return structuredClone({ bindingHash: snapshot.bindingHash, + profilePublication: snapshot.profilePublication ?? null, binding, modelToolName: bound.name, reviewedTool: bound.tool, diff --git a/orchestrator/src/rpc/profiles.ts b/orchestrator/src/rpc/profiles.ts index 9d3d55590..96d52659a 100644 --- a/orchestrator/src/rpc/profiles.ts +++ b/orchestrator/src/rpc/profiles.ts @@ -112,11 +112,18 @@ export interface ProfileDeps { discoverRepos?: (profileId: string) => Promise; } +function publication(expected: bigint | undefined, actorId: string) { + if (expected === undefined) return undefined; + if (expected < 0n || expected > BigInt(Number.MAX_SAFE_INTEGER)) + throw new ConnectError("invalid MCP grant revision", Code.InvalidArgument); + return { actorId, expectedRevision: Number(expected) }; +} /** Map a ProfileRow to the proto Profile. env_vars included only when admin. */ function toProto(row: ProfileRow, isAdmin: boolean): Profile { return { id: row.id, + mcpGrantRevision: BigInt(row.mcpGrantPublication?.revision ?? 0), name: row.name, description: row.description, icon: row.icon, @@ -492,7 +499,12 @@ export function registerProfiles(router: ConnectRouter, deps?: ProfileDeps): voi operation?: string; resourceConstraints?: string[]; credentialScope?: string; - mcp?: { upstreamName: string; automationWrite: boolean; argumentConstraintDocumentsJson: string[]; unrestricted: boolean }; + mcp?: { + upstreamName: string; + automationWrite: boolean; + argumentConstraintDocumentsJson: string[]; + unrestricted: boolean; + }; }>, ): ProfileIntegrationGrant[] { return grants.map((grant) => { @@ -559,88 +571,99 @@ export function registerProfiles(router: ConnectRouter, deps?: ProfileDeps): voi async createProfile(req, ctx) { const user = await requireUser(ctx, getSession); const ability = abilityFor(user); - if (!ability.can("manage", "Profile")) throw new ConnectError("forbidden", Code.PermissionDenied); + if (!ability.can("manage", "Profile")) + throw new ConnectError("forbidden", Code.PermissionDenied); if (!req.name.trim()) throw new ConnectError("name is required", Code.InvalidArgument); await assertImageEnabled(req.imageId); const model = catalogOptionId(req.model); const modelRouter = catalogOptionId(req.modelRouter); const effort = catalogOptionId(req.effort); - const harness = await assertHarnessValid(catalogOptionId(req.harness), modelRouter, model, effort); + const harness = await assertHarnessValid( + catalogOptionId(req.harness), + modelRouter, + model, + effort, + ); await assertSkillsValid(req.skills ?? []); const integrationGrants = normalizeIntegrationGrants(req.integrationGrants ?? []); const resolvedGrants = await resolveIntegrationGrants(integrationGrants, connections); - assertResolvedGrantsValid( - resolvedGrants, - await loadRegistry(connectors), - toolCapabilities(), - ); + assertResolvedGrantsValid(resolvedGrants, await loadRegistry(connectors), toolCapabilities()); const network = normalizeNetwork(req.network); const secrets = normalizeSecrets(req.secrets ?? []); assertNetworkValid(network); assertSecretsValid(secrets); - const row = await store.create({ - name: req.name, - description: req.description, - icon: req.icon || "Bot", - imageId: req.imageId, - programmaticOnly: req.programmaticOnly, - includeUserTokens: req.includeUserTokens, - envVars: req.envVars ?? {}, - skills: req.skills ?? [], - integrationGrants, - network, - secrets, - repos: normalizeReposChecked(req.repos ?? []), - harness, - modelRouter, - model, - effort, - apps: normalizeAppsChecked(req.apps ?? []), - }); + const row = await store.create( + { + name: req.name, + description: req.description, + icon: req.icon || "Bot", + imageId: req.imageId, + programmaticOnly: req.programmaticOnly, + includeUserTokens: req.includeUserTokens, + envVars: req.envVars ?? {}, + skills: req.skills ?? [], + integrationGrants, + network, + secrets, + repos: normalizeReposChecked(req.repos ?? []), + harness, + modelRouter, + model, + effort, + apps: normalizeAppsChecked(req.apps ?? []), + }, + publication(req.expectedMcpGrantRevision, user.id), + ); return { profile: toProto(row, true) }; }, async updateProfile(req, ctx) { const user = await requireUser(ctx, getSession); const ability = abilityFor(user); - if (!ability.can("manage", "Profile")) throw new ConnectError("forbidden", Code.PermissionDenied); + if (!ability.can("manage", "Profile")) + throw new ConnectError("forbidden", Code.PermissionDenied); if (!req.name.trim()) throw new ConnectError("name is required", Code.InvalidArgument); await assertImageEnabled(req.imageId); const model = catalogOptionId(req.model); const modelRouter = catalogOptionId(req.modelRouter); const effort = catalogOptionId(req.effort); - const harness = await assertHarnessValid(catalogOptionId(req.harness), modelRouter, model, effort); + const harness = await assertHarnessValid( + catalogOptionId(req.harness), + modelRouter, + model, + effort, + ); await assertSkillsValid(req.skills ?? []); const integrationGrants = normalizeIntegrationGrants(req.integrationGrants ?? []); const resolvedGrants = await resolveIntegrationGrants(integrationGrants, connections); - assertResolvedGrantsValid( - resolvedGrants, - await loadRegistry(connectors), - toolCapabilities(), - ); + assertResolvedGrantsValid(resolvedGrants, await loadRegistry(connectors), toolCapabilities()); const network = normalizeNetwork(req.network); const secrets = normalizeSecrets(req.secrets ?? []); assertNetworkValid(network); assertSecretsValid(secrets); - const row = await store.update(req.id, { - name: req.name, - description: req.description, - icon: req.icon || "Bot", - imageId: req.imageId, - programmaticOnly: req.programmaticOnly, - includeUserTokens: req.includeUserTokens, - envVars: req.envVars ?? {}, - skills: req.skills ?? [], - integrationGrants, - network, - secrets, - repos: normalizeReposChecked(req.repos ?? []), - harness, - modelRouter, - model, - effort, - apps: normalizeAppsChecked(req.apps ?? []), - }); + const row = await store.update( + req.id, + { + name: req.name, + description: req.description, + icon: req.icon || "Bot", + imageId: req.imageId, + programmaticOnly: req.programmaticOnly, + includeUserTokens: req.includeUserTokens, + envVars: req.envVars ?? {}, + skills: req.skills ?? [], + integrationGrants, + network, + secrets, + repos: normalizeReposChecked(req.repos ?? []), + harness, + modelRouter, + model, + effort, + apps: normalizeAppsChecked(req.apps ?? []), + }, + publication(req.expectedMcpGrantRevision, user.id), + ); if (!row) throw new ConnectError("not found", Code.NotFound); return { profile: toProto(row, true) }; }, diff --git a/web/src/gen/engram/app/v1/profile_pb.ts b/web/src/gen/engram/app/v1/profile_pb.ts index 84555fcb6..0ab89ac6d 100644 --- a/web/src/gen/engram/app/v1/profile_pb.ts +++ b/web/src/gen/engram/app/v1/profile_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/profile.proto. */ export const file_engram_app_v1_profile: GenFile = /*@__PURE__*/ - fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEi6QUKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJKBAgMEA1KBAgPEBBKBAgTEBRKBAgUEBVSC2Rlc2lnbmF0aW9uIigKClNlc3Npb25BcHASDAoEbmFtZRgBIAEoCRIMCgRwb3J0GAIgASgNInEKC1Byb2ZpbGVSZXBvEgwKBHBhdGgYASABKAkSEgoKcmVtb3RlX3VybBgCIAEoCRI1CgZyZW1vdGUYAyABKAsyIC5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvUmVtb3RlSACIAQFCCQoHX3JlbW90ZSI+ChFQcm9maWxlUmVwb1JlbW90ZRIMCgRob3N0GAEgASgJEg0KBW93bmVyGAIgASgJEgwKBG5hbWUYAyABKAkiMQobRGlzY292ZXJQcm9maWxlUmVwb3NSZXF1ZXN0EhIKCnByb2ZpbGVfaWQYASABKAkiUAoORGlzY292ZXJlZFJlcG8SDAoEcGF0aBgBIAEoCRIwCgdyZW1vdGVzGAIgAygLMh8uZW5ncmFtLmFwcC52MS5EaXNjb3ZlcmVkUmVtb3RlIm8KEERpc2NvdmVyZWRSZW1vdGUSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSNQoGcGFyc2VkGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19wYXJzZWQiTAocRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRIsCgVyZXBvcxgBIAMoCzIdLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlcG8ipQEKF1Byb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EhUKDWNvbm5lY3Rpb25faWQYASABKAkSEQoJb3BlcmF0aW9uGAIgASgJEhwKFHJlc291cmNlX2NvbnN0cmFpbnRzGAMgAygJEhgKEGNyZWRlbnRpYWxfc2NvcGUYBCABKAkSKAoDbWNwGAUgASgLMhsuZW5ncmFtLmFwcC52MS5NY3BUb29sR3JhbnQiUwoOUHJvZmlsZU5ldHdvcmsSDwoHZGVmYXVsdBgBIAEoCRITCgthbGxvd19ob3N0cxgCIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAMgAygJIm0KDVByb2ZpbGVTZWNyZXQSCwoDcmVmGAEgASgJEg8KB2Vudl92YXIYAiABKAkSDAoEbW9kZRgDIAEoCRITCgthbGxvd19ob3N0cxgEIAMoCRIbChNhbGxvd19ob3N0X3BhdHRlcm5zGAUgAygJIm4KD1Byb2ZpbGVTbmFwc2hvdBIKCgJpZBgBIAEoCRIMCgRuYW1lGAIgASgJEgwKBGljb24YAyABKAkSEAoIYXJjaGl2ZWQYBCABKAgSEQoJaW1hZ2VfdXJpGAUgASgJEg4KBnNraWxscxgGIAMoCSJLChNMaXN0UHJvZmlsZXNSZXF1ZXN0EhgKEGluY2x1ZGVfYXJjaGl2ZWQYASABKAgSGgoSZm9yX3Nlc3Npb25fcGlja2VyGAIgASgIIkAKFExpc3RQcm9maWxlc1Jlc3BvbnNlEigKCHByb2ZpbGVzGAEgAygLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIh8KEUdldFByb2ZpbGVSZXF1ZXN0EgoKAmlkGAEgASgJIj0KEkdldFByb2ZpbGVSZXNwb25zZRInCgdwcm9maWxlGAEgASgLMhYuZW5ncmFtLmFwcC52MS5Qcm9maWxlIrAFChRDcmVhdGVQcm9maWxlUmVxdWVzdBIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEgwKBGljb24YAyABKAkSEAoIaW1hZ2VfaWQYBCABKAkSGwoTaW5jbHVkZV91c2VyX3Rva2VucxgFIAEoCBJCCghlbnZfdmFycxgGIAMoCzIwLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QuRW52VmFyc0VudHJ5Eg4KBnNraWxscxgHIAMoCRIuCgduZXR3b3JrGAkgASgLMh0uZW5ncmFtLmFwcC52MS5Qcm9maWxlTmV0d29yaxItCgdzZWNyZXRzGAogAygLMhwuZW5ncmFtLmFwcC52MS5Qcm9maWxlU2VjcmV0EhQKB2hhcm5lc3MYDCABKAlIAIgBARISCgVtb2RlbBgNIAEoCUgBiAEBEhMKBmVmZm9ydBgOIAEoCUgCiAEBEkIKEmludGVncmF0aW9uX2dyYW50cxgRIAMoCzImLmVuZ3JhbS5hcHAudjEuUHJvZmlsZUludGVncmF0aW9uR3JhbnQSKQoFcmVwb3MYEiADKAsyGi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVSZXBvEhkKDG1vZGVsX3JvdXRlchgTIAEoCUgDiAEBEicKBGFwcHMYFCADKAsyGS5lbmdyYW0uYXBwLnYxLlNlc3Npb25BcHASGQoRcHJvZ3JhbW1hdGljX29ubHkYFSABKAgaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSLXBQoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlKBAgJEApKBAgMEA1KBAgQEBFKBAgREBIiQAoVVXBkYXRlUHJvZmlsZVJlc3BvbnNlEicKB3Byb2ZpbGUYASABKAsyFi5lbmdyYW0uYXBwLnYxLlByb2ZpbGUiIgoURGVsZXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkiFwoVRGVsZXRlUHJvZmlsZVJlc3BvbnNlIqIBCgxNY3BUb29sR3JhbnQSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRIYChBhdXRvbWF0aW9uX3dyaXRlGAIgASgIEhQKDHVucmVzdHJpY3RlZBgEIAEoCBIqCiJhcmd1bWVudF9jb25zdHJhaW50X2RvY3VtZW50c19qc29uGAUgAygJSgQIAxAEUhlhcmd1bWVudF9jb25zdHJhaW50c19qc29uMsEECg5Qcm9maWxlU2VydmljZRJXCgxMaXN0UHJvZmlsZXMSIi5lbmdyYW0uYXBwLnYxLkxpc3RQcm9maWxlc1JlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkxpc3RQcm9maWxlc1Jlc3BvbnNlElEKCkdldFByb2ZpbGUSIC5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5HZXRQcm9maWxlUmVzcG9uc2USWgoNQ3JlYXRlUHJvZmlsZRIjLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVByb2ZpbGVSZXNwb25zZRJaCg1VcGRhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVXBkYXRlUHJvZmlsZVJlc3BvbnNlEm8KFERpc2NvdmVyUHJvZmlsZVJlcG9zEiouZW5ncmFtLmFwcC52MS5EaXNjb3ZlclByb2ZpbGVSZXBvc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVzcG9uc2USWgoNRGVsZXRlUHJvZmlsZRIjLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkRlbGV0ZVByb2ZpbGVSZXNwb25zZWIGcHJvdG8z"); + fileDesc("ChtlbmdyYW0vYXBwL3YxL3Byb2ZpbGUucHJvdG8SDWVuZ3JhbS5hcHAudjEihQYKB1Byb2ZpbGUSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSNQoIZW52X3ZhcnMYByADKAsyIy5lbmdyYW0uYXBwLnYxLlByb2ZpbGUuRW52VmFyc0VudHJ5EhAKCGFyY2hpdmVkGAggASgIEhIKCmNyZWF0ZWRfYXQYCSABKAkSEgoKdXBkYXRlZF9hdBgKIAEoCRIOCgZza2lsbHMYCyADKAkSLgoHbmV0d29yaxgNIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgOIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGBAgASgJSACIAQESEgoFbW9kZWwYESABKAlIAYgBARITCgZlZmZvcnQYEiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYFSADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBYgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFyABKAlIA4gBARInCgRhcHBzGBggAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBkgASgIEhoKEm1jcF9ncmFudF9yZXZpc2lvbhgaIAEoAxouCgxFbnZWYXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIKCghfaGFybmVzc0IICgZfbW9kZWxCCQoHX2VmZm9ydEIPCg1fbW9kZWxfcm91dGVySgQIDBANSgQIDxAQSgQIExAUSgQIFBAVUgtkZXNpZ25hdGlvbiIoCgpTZXNzaW9uQXBwEgwKBG5hbWUYASABKAkSDAoEcG9ydBgCIAEoDSJxCgtQcm9maWxlUmVwbxIMCgRwYXRoGAEgASgJEhIKCnJlbW90ZV91cmwYAiABKAkSNQoGcmVtb3RlGAMgASgLMiAuZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwb1JlbW90ZUgAiAEBQgkKB19yZW1vdGUiPgoRUHJvZmlsZVJlcG9SZW1vdGUSDAoEaG9zdBgBIAEoCRINCgVvd25lchgCIAEoCRIMCgRuYW1lGAMgASgJIjEKG0Rpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBISCgpwcm9maWxlX2lkGAEgASgJIlAKDkRpc2NvdmVyZWRSZXBvEgwKBHBhdGgYASABKAkSMAoHcmVtb3RlcxgCIAMoCzIfLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJlZFJlbW90ZSJvChBEaXNjb3ZlcmVkUmVtb3RlEgwKBG5hbWUYASABKAkSCwoDdXJsGAIgASgJEjUKBnBhcnNlZBgDIAEoCzIgLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVJlcG9SZW1vdGVIAIgBAUIJCgdfcGFyc2VkIkwKHERpc2NvdmVyUHJvZmlsZVJlcG9zUmVzcG9uc2USLAoFcmVwb3MYASADKAsyHS5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyZWRSZXBvIqUBChdQcm9maWxlSW50ZWdyYXRpb25HcmFudBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhEKCW9wZXJhdGlvbhgCIAEoCRIcChRyZXNvdXJjZV9jb25zdHJhaW50cxgDIAMoCRIYChBjcmVkZW50aWFsX3Njb3BlGAQgASgJEigKA21jcBgFIAEoCzIbLmVuZ3JhbS5hcHAudjEuTWNwVG9vbEdyYW50IlMKDlByb2ZpbGVOZXR3b3JrEg8KB2RlZmF1bHQYASABKAkSEwoLYWxsb3dfaG9zdHMYAiADKAkSGwoTYWxsb3dfaG9zdF9wYXR0ZXJucxgDIAMoCSJtCg1Qcm9maWxlU2VjcmV0EgsKA3JlZhgBIAEoCRIPCgdlbnZfdmFyGAIgASgJEgwKBG1vZGUYAyABKAkSEwoLYWxsb3dfaG9zdHMYBCADKAkSGwoTYWxsb3dfaG9zdF9wYXR0ZXJucxgFIAMoCSJuCg9Qcm9maWxlU25hcHNob3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRpY29uGAMgASgJEhAKCGFyY2hpdmVkGAQgASgIEhEKCWltYWdlX3VyaRgFIAEoCRIOCgZza2lsbHMYBiADKAkiSwoTTGlzdFByb2ZpbGVzUmVxdWVzdBIYChBpbmNsdWRlX2FyY2hpdmVkGAEgASgIEhoKEmZvcl9zZXNzaW9uX3BpY2tlchgCIAEoCCJAChRMaXN0UHJvZmlsZXNSZXNwb25zZRIoCghwcm9maWxlcxgBIAMoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSIfChFHZXRQcm9maWxlUmVxdWVzdBIKCgJpZBgBIAEoCSI9ChJHZXRQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSL6BQoUQ3JlYXRlUHJvZmlsZVJlcXVlc3QSDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIMCgRpY29uGAMgASgJEhAKCGltYWdlX2lkGAQgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBSABKAgSQgoIZW52X3ZhcnMYBiADKAsyMC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYByADKAkSLgoHbmV0d29yaxgJIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgKIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGAwgASgJSACIAQESEgoFbW9kZWwYDSABKAlIAYgBARITCgZlZmZvcnQYDiABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYESADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBIgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYEyABKAlIA4gBARInCgRhcHBzGBQgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEhkKEXByb2dyYW1tYXRpY19vbmx5GBUgASgIEigKG2V4cGVjdGVkX21jcF9ncmFudF9yZXZpc2lvbhgWIAEoA0gEiAEBGi4KDEVudlZhcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgoKCF9oYXJuZXNzQggKBl9tb2RlbEIJCgdfZWZmb3J0Qg8KDV9tb2RlbF9yb3V0ZXJCHgocX2V4cGVjdGVkX21jcF9ncmFudF9yZXZpc2lvbkoECAgQCUoECAsQDEoECA8QEEoECBAQESJAChVDcmVhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSKhBgoUVXBkYXRlUHJvZmlsZVJlcXVlc3QSCgoCaWQYASABKAkSDAoEbmFtZRgCIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIMCgRpY29uGAQgASgJEhAKCGltYWdlX2lkGAUgASgJEhsKE2luY2x1ZGVfdXNlcl90b2tlbnMYBiABKAgSQgoIZW52X3ZhcnMYByADKAsyMC5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0LkVudlZhcnNFbnRyeRIOCgZza2lsbHMYCCADKAkSLgoHbmV0d29yaxgKIAEoCzIdLmVuZ3JhbS5hcHAudjEuUHJvZmlsZU5ldHdvcmsSLQoHc2VjcmV0cxgLIAMoCzIcLmVuZ3JhbS5hcHAudjEuUHJvZmlsZVNlY3JldBIUCgdoYXJuZXNzGA0gASgJSACIAQESEgoFbW9kZWwYDiABKAlIAYgBARITCgZlZmZvcnQYDyABKAlIAogBARJCChJpbnRlZ3JhdGlvbl9ncmFudHMYEiADKAsyJi5lbmdyYW0uYXBwLnYxLlByb2ZpbGVJbnRlZ3JhdGlvbkdyYW50EikKBXJlcG9zGBMgAygLMhouZW5ncmFtLmFwcC52MS5Qcm9maWxlUmVwbxIZCgxtb2RlbF9yb3V0ZXIYFCABKAlIA4gBARInCgRhcHBzGBUgAygLMhkuZW5ncmFtLmFwcC52MS5TZXNzaW9uQXBwEh4KEXByb2dyYW1tYXRpY19vbmx5GBYgASgISASIAQESKAobZXhwZWN0ZWRfbWNwX2dyYW50X3JldmlzaW9uGBcgASgDSAWIAQEaLgoMRW52VmFyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAFCCgoIX2hhcm5lc3NCCAoGX21vZGVsQgkKB19lZmZvcnRCDwoNX21vZGVsX3JvdXRlckIUChJfcHJvZ3JhbW1hdGljX29ubHlCHgocX2V4cGVjdGVkX21jcF9ncmFudF9yZXZpc2lvbkoECAkQCkoECAwQDUoECBAQEUoECBEQEiJAChVVcGRhdGVQcm9maWxlUmVzcG9uc2USJwoHcHJvZmlsZRgBIAEoCzIWLmVuZ3JhbS5hcHAudjEuUHJvZmlsZSIiChREZWxldGVQcm9maWxlUmVxdWVzdBIKCgJpZBgBIAEoCSIXChVEZWxldGVQcm9maWxlUmVzcG9uc2UiogEKDE1jcFRvb2xHcmFudBIVCg11cHN0cmVhbV9uYW1lGAEgASgJEhgKEGF1dG9tYXRpb25fd3JpdGUYAiABKAgSFAoMdW5yZXN0cmljdGVkGAQgASgIEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YBSADKAlKBAgDEARSGWFyZ3VtZW50X2NvbnN0cmFpbnRzX2pzb24ywQQKDlByb2ZpbGVTZXJ2aWNlElcKDExpc3RQcm9maWxlcxIiLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBojLmVuZ3JhbS5hcHAudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USUQoKR2V0UHJvZmlsZRIgLmVuZ3JhbS5hcHAudjEuR2V0UHJvZmlsZVJlcXVlc3QaIS5lbmdyYW0uYXBwLnYxLkdldFByb2ZpbGVSZXNwb25zZRJaCg1DcmVhdGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5DcmVhdGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuQ3JlYXRlUHJvZmlsZVJlc3BvbnNlEloKDVVwZGF0ZVByb2ZpbGUSIy5lbmdyYW0uYXBwLnYxLlVwZGF0ZVByb2ZpbGVSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5VcGRhdGVQcm9maWxlUmVzcG9uc2USbwoURGlzY292ZXJQcm9maWxlUmVwb3MSKi5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyUHJvZmlsZVJlcG9zUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJQcm9maWxlUmVwb3NSZXNwb25zZRJaCg1EZWxldGVQcm9maWxlEiMuZW5ncmFtLmFwcC52MS5EZWxldGVQcm9maWxlUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGVsZXRlUHJvZmlsZVJlc3BvbnNlYgZwcm90bzM"); /** * An admin-curated bundle of session-launch inputs (ADR §1). @@ -169,6 +169,13 @@ export type Profile = Message<"engram.app.v1.Profile"> & { * @generated from field: bool programmatic_only = 25; */ programmaticOnly: boolean; + + /** + * Revision for explicit MCP grant publication; zero means no publication. + * + * @generated from field: int64 mcp_grant_revision = 26; + */ + mcpGrantRevision: bigint; }; /** @@ -722,6 +729,13 @@ export type CreateProfileRequest = Message<"engram.app.v1.CreateProfileRequest"> * @generated from field: bool programmatic_only = 21; */ programmaticOnly: boolean; + + /** + * Presence requests explicit MCP grant publication. First publication expects zero. + * + * @generated from field: optional int64 expected_mcp_grant_revision = 22; + */ + expectedMcpGrantRevision?: bigint; }; /** @@ -852,6 +866,13 @@ export type UpdateProfileRequest = Message<"engram.app.v1.UpdateProfileRequest"> * @generated from field: optional bool programmatic_only = 22; */ programmaticOnly?: boolean; + + /** + * Presence requests publication or republication of the exact MCP grants. + * + * @generated from field: optional int64 expected_mcp_grant_revision = 23; + */ + expectedMcpGrantRevision?: bigint; }; /** diff --git a/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts b/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts index 32996e105..2856683a0 100644 --- a/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts +++ b/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts @@ -19,6 +19,11 @@ export const readWritePolicy = SessionPolicyService.method.readWritePolicy; */ export const acceptPlanExit = SessionPolicyService.method.acceptPlanExit; +/** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ValidateExecutionIdentity + */ +export const validateExecutionIdentity = SessionPolicyService.method.validateExecutionIdentity; + /** * @generated from rpc engram.mcp.v1.SessionPolicyService.ValidateDispatchFence */ diff --git a/web/src/gen/engram/mcp/v1/session_policy_pb.ts b/web/src/gen/engram/mcp/v1/session_policy_pb.ts index a2c12482d..e15af2eab 100644 --- a/web/src/gen/engram/mcp/v1/session_policy_pb.ts +++ b/web/src/gen/engram/mcp/v1/session_policy_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/session_policy.proto. */ export const file_engram_mcp_v1_session_policy: GenFile = /*@__PURE__*/ - fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIo0CChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYiQQodVmFsaWRhdGVEaXNwYXRjaEZlbmNlUmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SDgoGc3RhdHVzGAIgASgJIlQKG0NyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRI1CgdzZXNzaW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5DcmVhdGVTZXNzaW9uUmVzcG9uc2UiUgoXUmVhZFdyaXRlUG9saWN5UmVzcG9uc2USNwoFc3RhdGUYASABKAsyKC5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVzcG9uc2UiTgoWQWNjZXB0UGxhbkV4aXRSZXNwb25zZRI0CgZwb2xpY3kYASABKAsyJC5lbmdyYW0uYXBwLnYxLk1jcFRydXN0ZWRXcml0ZVBvbGljeTK5AwoUU2Vzc2lvblBvbGljeVNlcnZpY2USbAoTQ3JlYXRlUG9saWN5U2Vzc2lvbhIpLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlcXVlc3QaKi5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRJgCg9SZWFkV3JpdGVQb2xpY3kSJS5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlcXVlc3QaJi5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlc3BvbnNlEl0KDkFjY2VwdFBsYW5FeGl0EiQuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlcXVlc3QaJS5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVzcG9uc2UScgoVVmFsaWRhdGVEaXNwYXRjaEZlbmNlEisuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZWIGcHJvdG8z", [file_engram_app_v1_session]); + fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCTK5BAoUU2Vzc2lvblBvbGljeVNlcnZpY2USbAoTQ3JlYXRlUG9saWN5U2Vzc2lvbhIpLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlcXVlc3QaKi5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRJgCg9SZWFkV3JpdGVQb2xpY3kSJS5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlcXVlc3QaJi5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlc3BvbnNlEl0KDkFjY2VwdFBsYW5FeGl0EiQuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlcXVlc3QaJS5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVzcG9uc2USfgoZVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eRIvLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlcXVlc3QaMC5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHlSZXNwb25zZRJyChVWYWxpZGF0ZURpc3BhdGNoRmVuY2USKy5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRGlzcGF0Y2hGZW5jZVJlcXVlc3QaLC5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRGlzcGF0Y2hGZW5jZVJlc3BvbnNlYgZwcm90bzM", [file_engram_app_v1_session]); /** * @generated from message engram.mcp.v1.CreatePolicySessionRequest @@ -148,6 +148,16 @@ export type ValidateDispatchFenceRequest = Message<"engram.mcp.v1.ValidateDispat * @generated from field: repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 7; */ ancestorPolicyRefs: McpWritePolicyRef[]; + + /** + * @generated from field: string model_tool_name = 8; + */ + modelToolName: string; + + /** + * @generated from field: string original_arguments = 9; + */ + originalArguments: string; }; /** @@ -230,6 +240,85 @@ export type AcceptPlanExitResponse = Message<"engram.mcp.v1.AcceptPlanExitRespon export const AcceptPlanExitResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_session_policy, 7); +/** + * @generated from message engram.mcp.v1.ValidateExecutionIdentityRequest + */ +export type ValidateExecutionIdentityRequest = Message<"engram.mcp.v1.ValidateExecutionIdentityRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string session_id = 2; + */ + sessionId: string; + + /** + * @generated from field: string run_id = 3; + */ + runId: string; + + /** + * @generated from field: string tool_call_id = 4; + */ + toolCallId: string; + + /** + * @generated from field: engram.app.v1.McpWritePolicyRef accepted_policy_ref = 5; + */ + acceptedPolicyRef?: McpWritePolicyRef; + + /** + * @generated from field: string accepted_write_eligibility = 6; + */ + acceptedWriteEligibility: string; + + /** + * @generated from field: repeated engram.app.v1.McpWritePolicyRef ancestor_policy_refs = 7; + */ + ancestorPolicyRefs: McpWritePolicyRef[]; + + /** + * @generated from field: string model_tool_name = 8; + */ + modelToolName: string; + + /** + * @generated from field: string original_arguments = 9; + */ + originalArguments: string; +}; + +/** + * Describes the message engram.mcp.v1.ValidateExecutionIdentityRequest. + * Use `create(ValidateExecutionIdentityRequestSchema)` to create a new message. + */ +export const ValidateExecutionIdentityRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 8); + +/** + * @generated from message engram.mcp.v1.ValidateExecutionIdentityResponse + */ +export type ValidateExecutionIdentityResponse = Message<"engram.mcp.v1.ValidateExecutionIdentityResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: string status = 2; + */ + status: string; +}; + +/** + * Describes the message engram.mcp.v1.ValidateExecutionIdentityResponse. + * Use `create(ValidateExecutionIdentityResponseSchema)` to create a new message. + */ +export const ValidateExecutionIdentityResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 9); + /** * Dedicated pinned mTLS only. Create reuses the ordinary creation core. * @@ -260,6 +349,14 @@ export const SessionPolicyService: GenService<{ input: typeof AcceptPlanExitRequestSchema; output: typeof AcceptPlanExitResponseSchema; }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ValidateExecutionIdentity + */ + validateExecutionIdentity: { + methodKind: "unary"; + input: typeof ValidateExecutionIdentityRequestSchema; + output: typeof ValidateExecutionIdentityResponseSchema; + }, /** * @generated from rpc engram.mcp.v1.SessionPolicyService.ValidateDispatchFence */ From 2050755cb205186774768b9893e9757664a28ca7 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 04:20:14 -0500 Subject: [PATCH 023/279] Bound MCP admission capacity reads to live reservations --- .../__tests__/mcp-phased-admission.test.ts | 102 ++++++++++++++++++ orchestrator/src/mcp/admission.ts | 10 +- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/orchestrator/src/__tests__/mcp-phased-admission.test.ts b/orchestrator/src/__tests__/mcp-phased-admission.test.ts index bc75208fc..accac34f2 100644 --- a/orchestrator/src/__tests__/mcp-phased-admission.test.ts +++ b/orchestrator/src/__tests__/mcp-phased-admission.test.ts @@ -195,3 +195,105 @@ test("schema capacity is shared across database clients and released capacity pr for (const f of fixtures) await f.cleanup(); } }); + +test("capacity reads exclude retained history but count expired active leases until release", async () => { + const f = await reviewFixture(); + const { drizzle } = await import("drizzle-orm/node-postgres"); + const { getPool } = await import("../db/client.ts"); + const schema = await import("../db/schema.ts"); + const { makeMcpAdmission } = await import("../mcp/admission.ts"); + const capacityReads: { query: string; params: unknown[] }[] = []; + const capacity = makeMcpAdmission( + drizzle(getPool(), { + schema, + logger: { + logQuery(query, params) { + if ( + query.startsWith("select") && + query.includes('from "mcp_admission"') && + query.includes('"mcp_admission"."deployment_id" =') + ) { + capacityReads.push({ query, params }); + } + }, + }, + }), + ); + try { + const calls = []; + for (let i = 0; i < 7; i++) { + const toolCallId = `capacity-${i}`; + const call = await f.journal.create( + { + ...f.input, + toolCallId, + requestPolicyContext: { ...f.input.requestPolicyContext, toolCallId }, + }, + { kind: "approval" }, + f.now, + ); + calls.push(call); + if (i === 6) continue; + await f.journal.admitSchema(call.id, f.deploymentId, f.now); + if (i < 2) { + const lease = (await f.journal.getAdmission(call.id))!; + await f.journal.finishSchema( + call.id, + lease.reservationId, + lease.expiresAt, + call.requestDigest, + i === 0, + f.now, + ); + } + } + const successfulHistory = (await f.journal.getAdmission(calls[0]!.id))!; + const failedHistory = (await f.journal.getAdmission(calls[1]!.id))!; + const later = new Date(f.now.getTime() + 15_000); + // The four active leases are expired, but still own capacity until explicit release. + expect(await capacity.reserve(calls[6]!.id, f.deploymentId, "schema", later)).toBe("queued"); + const waiting = (await f.journal.getAdmission(calls[6]!.id))!; + expect(capacityReads).toHaveLength(1); + const read = capacityReads[0]!; + // Replay the actual emitted SQL: four active rows plus the newly queued row, no history. + const population = await getPool().query<{ state: string; execution_id: string }>( + read.query, + read.params, + ); + expect(population.rows).toHaveLength(5); + expect(population.rows.filter((row) => row.state === "admitted")).toHaveLength(4); + expect(population.rows.filter((row) => row.state === "queued")).toHaveLength(1); + expect(population.rows.map((row) => row.execution_id)).not.toContain(calls[0]!.id); + expect(population.rows.map((row) => row.execution_id)).not.toContain(calls[1]!.id); + expect(await f.journal.getAdmission(calls[0]!.id)).toEqual(successfulHistory); + expect(await f.journal.getAdmission(calls[1]!.id)).toEqual(failedHistory); + const active = (await f.journal.getAdmission(calls[2]!.id))!; + await f.journal.finishSchema( + calls[2]!.id, + active.reservationId, + active.expiresAt, + calls[2]!.requestDigest, + false, + later, + ); + const promotionTime = new Date(f.now.getTime() + 20_000); + expect(await capacity.reserve(calls[6]!.id, f.deploymentId, "schema", promotionTime)).toBe( + "admitted", + ); + expect((await f.journal.getAdmission(calls[6]!.id))!.schemaQueueExpiresAt).toEqual( + waiting.schemaQueueExpiresAt, + ); + // The separately fetched released row still carries its original retry deadline. + expect(await capacity.reserve(calls[1]!.id, f.deploymentId, "schema", promotionTime)).toBe( + "queued", + ); + const replacement = (await f.journal.getAdmission(calls[1]!.id))!; + expect(replacement.schemaQueueExpiresAt).toEqual(failedHistory.schemaQueueExpiresAt); + expect(replacement.reservationId).not.toBe(failedHistory.reservationId); + expect((await f.journal.getAdmission(calls[0]!.id))!.schemaDigest).toBe( + calls[0]!.requestDigest, + ); + } finally { + await f.cleanup(); + } +}); diff --git a/orchestrator/src/mcp/admission.ts b/orchestrator/src/mcp/admission.ts index c6bfeb571..88822409a 100644 --- a/orchestrator/src/mcp/admission.ts +++ b/orchestrator/src/mcp/admission.ts @@ -1,4 +1,4 @@ -import { and, asc, eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import { getDb } from "../db/client.ts"; import { mcpAdmission as admission, @@ -56,8 +56,12 @@ export function makeMcpAdmission(db = getDb()) { const rows = await tx .select() .from(admission) - .where(eq(admission.deploymentId, deploymentId)) - .orderBy(asc(admission.createdAt)); + .where( + and( + eq(admission.deploymentId, deploymentId), + inArray(admission.state, ["queued", "admitted"]), + ), + ); const active = rows.filter((row) => row.state === "admitted"); const available = active.length < MCP_LIMITS.deploymentConcurrency && From 23f0626d1709e9d59b634bdc4e5ccff1514042b4 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 04:42:32 -0500 Subject: [PATCH 024/279] Drive MCP effects through durable admission and live authority --- mcp-worker/test/fixtures/effect-worker.ts | 111 + .../drizzle/0110_mcp_preflight_rejection.sql | 18 + orchestrator/drizzle/meta/0110_snapshot.json | 8514 +++++++++++++++++ orchestrator/drizzle/meta/_journal.json | 7 + .../src/__tests__/mcp-effect-driver.test.ts | 585 ++ .../src/__tests__/mcp-effect-fixture.ts | 337 + .../src/__tests__/mcp-effect-result.test.ts | 56 + .../__tests__/mcp-preflight-rejection.test.ts | 109 + orchestrator/src/__tests__/mcp-rpc.test.ts | 1 + orchestrator/src/mcp/effect-driver.ts | 513 + orchestrator/src/mcp/journal.ts | 76 + 11 files changed, 10327 insertions(+) create mode 100644 mcp-worker/test/fixtures/effect-worker.ts create mode 100644 orchestrator/drizzle/0110_mcp_preflight_rejection.sql create mode 100644 orchestrator/drizzle/meta/0110_snapshot.json create mode 100644 orchestrator/src/__tests__/mcp-effect-driver.test.ts create mode 100644 orchestrator/src/__tests__/mcp-effect-fixture.ts create mode 100644 orchestrator/src/__tests__/mcp-effect-result.test.ts create mode 100644 orchestrator/src/__tests__/mcp-preflight-rejection.test.ts create mode 100644 orchestrator/src/mcp/effect-driver.ts diff --git a/mcp-worker/test/fixtures/effect-worker.ts b/mcp-worker/test/fixtures/effect-worker.ts new file mode 100644 index 000000000..2d5bab476 --- /dev/null +++ b/mcp-worker/test/fixtures/effect-worker.ts @@ -0,0 +1,111 @@ +// Actual worker and provider HTTPS servers in Node. DNS/dial routing is a test boundary. +import { createInterface } from "node:readline"; +import { createServer, request } from "node:https"; +import { createWorker } from "../../src/service.ts"; +import { ZonePolicy } from "../../src/network.ts"; +const lines = createInterface({ input: process.stdin }); +const input = lines[Symbol.asyncIterator](); +const { tls, response = "normal" } = JSON.parse((await input.next()).value!); +let calls = 0, + credentialRequests = 0; +const provider = createServer(tls, (req, res) => { + let raw = ""; + req.on("data", (chunk: Buffer) => { + raw += chunk; + }); + req.on("end", () => { + if (req.headers.authorization) credentialRequests++; + const message = JSON.parse(raw); + let result: unknown; + if (message.method === "server/discover") + result = { + supportedVersions: ["2026-07-28"], + capabilities: { tools: {} }, + }; + else if (message.method === "tools/call") { + calls++; + if (response === "hold") return; + if (response === "malformed") { + res.writeHead(200, { "content-type": "application/json" }); + res.end("{broken"); + return; + } + const secret = String(req.headers.authorization ?? ""); + const text = + response === "oversize" + ? "x".repeat(1100000) + : response === "normalized-limit" + ? "x".repeat(550000) + : `${secret} | ${secret.replace(/^Bearer /, "")}`; + result = { + content: + response === "image" + ? [{ type: "image", data: "AA==", mimeType: "image/png" }] + : [{ type: "text", text }], + structuredContent: { echo: text }, + isError: response === "error", + }; + } else { + res.writeHead(202); + res.end(); + return; + } + res.writeHead(200, { "content-type": "application/json" }); + res.end( + JSON.stringify({ + jsonrpc: "2.0", + id: message.id, + result: { ...Object(result), resultType: "complete" }, + }), + ); + }); +}); +await new Promise((resolve) => provider.listen(0, "127.0.0.1", resolve)); +const address = provider.address(); +if (!address || typeof address === "string") throw Error("fixture address"); +const zone = new ZonePolicy({ + id: "public", + revision: "1", + mode: "public", + authorities: ["tools.example.test"], + aliases: [], + cidrs: [], + denyCidrs: [], +}); +const make = () => + createWorker({ + tls, + zone, + enabled: true, + callers: [{ fingerprint: tls.fingerprint, role: "orchestrator" }], + network: { + resolve: async () => ["93.184.216.34"], + request: (_url, options) => + request(new URL(`https://127.0.0.1:${address.port}/mcp`), { + ...options, + ca: tls.ca, + servername: "localhost", + lookup: (_host, _options, callback) => callback(null, "127.0.0.1", 4), + }), + }, + }); +let worker = make(); +const port = await worker.listen(0, "127.0.0.1"); +const send = (value: unknown) => + process.stdout.write(`${JSON.stringify(value)}\n`); +send({ port, identity: worker.identity, node: process.versions.node }); +for await (const command of { [Symbol.asyncIterator]: () => input }) { + if (command === "stats") send({ calls, credentialRequests }); + else if (command === "restart") { + await worker.close(); + worker = make(); + await worker.listen(port, "127.0.0.1"); + send({ identity: worker.identity }); + } else if (command === "close") { + await worker.close(); + provider.closeAllConnections(); + await new Promise((resolve) => provider.close(() => resolve())); + lines.close(); + break; + } +} diff --git a/orchestrator/drizzle/0110_mcp_preflight_rejection.sql b/orchestrator/drizzle/0110_mcp_preflight_rejection.sql new file mode 100644 index 000000000..9b51016f3 --- /dev/null +++ b/orchestrator/drizzle/0110_mcp_preflight_rejection.sql @@ -0,0 +1,18 @@ +CREATE OR REPLACE FUNCTION mcp_execution_transition() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF (to_jsonb(NEW) - ARRAY['state','version','boot_id','intent_id','intent_expires_at','result','result_unavailable','updated_at']) IS DISTINCT FROM + (to_jsonb(OLD) - ARRAY['state','version','boot_id','intent_id','intent_expires_at','result','result_unavailable','updated_at']) THEN + RAISE EXCEPTION 'MCP request identity is immutable'; + END IF; + IF NOT ((OLD.state = 'waiting_approval' AND NEW.state IN ('authorized','canceled','failed')) OR + (OLD.state = 'authorized' AND NEW.state IN ('dispatching','canceled','failed')) OR + (OLD.state = 'dispatching' AND NEW.state IN ('succeeded','failed','outcome_unknown')) OR + (OLD.state = 'outcome_unknown' AND NEW.state IN ('succeeded','failed'))) OR NEW.version <> OLD.version + 1 THEN + RAISE EXCEPTION 'Invalid MCP execution transition'; + END IF; + IF OLD.intent_id IS NOT NULL AND (NEW.intent_id, NEW.boot_id, NEW.intent_expires_at) IS DISTINCT FROM (OLD.intent_id, OLD.boot_id, OLD.intent_expires_at) THEN + RAISE EXCEPTION 'MCP dispatch intent is immutable'; + END IF; + RETURN NEW; +END; +$$; diff --git a/orchestrator/drizzle/meta/0110_snapshot.json b/orchestrator/drizzle/meta/0110_snapshot.json new file mode 100644 index 000000000..e4609f94f --- /dev/null +++ b/orchestrator/drizzle/meta/0110_snapshot.json @@ -0,0 +1,8514 @@ +{ + "id": "21a2d61b-0099-4962-8ffc-85a12a935482", + "prevId": "d9212cb0-4a75-4b3f-9676-21600edc014f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "columnsFrom": [ + "reference_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "columnsFrom": [ + "artifact_id" + ], + "tableTo": "artifact", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "builtin_key is not null", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "columnsFrom": [ + "automation_id" + ], + "tableTo": "automation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "columnsFrom": [ + "automation_id" + ], + "tableTo": "automation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "status = 'open'", + "concurrently": false + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "columnsFrom": [ + "automation_id" + ], + "tableTo": "automation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "columnsFrom": [ + "automation_id" + ], + "tableTo": "automation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "columnsFrom": [ + "instance_id" + ], + "tableTo": "automation_instance", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "scheduled_for is not null", + "concurrently": false + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "columnsFrom": [ + "automation_id" + ], + "tableTo": "automation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "columnsFrom": [ + "task_id" + ], + "tableTo": "task", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "columnsFrom": [ + "run_id" + ], + "tableTo": "automation_run", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "columnsFrom": [ + "session_id" + ], + "tableTo": "automation_session", + "columnsTo": [ + "session_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "columnsFrom": [ + "automation_id" + ], + "tableTo": "automation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "columnsFrom": [ + "automation_id" + ], + "tableTo": "automation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "columnsFrom": [ + "run_id" + ], + "tableTo": "automation_run", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "columnsFrom": [ + "automation_id" + ], + "tableTo": "automation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_credential": { + "name": "mcp_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "is_default", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "columns": [ + "alias" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "integration_connection", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "state = 'active'", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()::text" + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'execute'" + }, + "schema_queue_expires_at": { + "name": "schema_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_queue_expires_at": { + "name": "execute_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_expires_at": { + "name": "execute_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "schema_digest": { + "name": "schema_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_reservation_idx": { + "name": "mcp_admission_reservation_idx", + "columns": [ + { + "expression": "reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "columnsFrom": [ + "execution_id" + ], + "tableTo": "mcp_execution", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "columnsFrom": [ + "deployment_id" + ], + "tableTo": "mcp_admission_scope", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "integration_connection", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_phase_check": { + "name": "mcp_admission_phase_check", + "value": "\"mcp_admission\".\"phase\" IN ('schema','execute')" + }, + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted','released')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "columnsFrom": [ + "execution_id" + ], + "tableTo": "mcp_execution", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "kind = 'published'", + "concurrently": false + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "integration_connection", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_arguments": { + "name": "original_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "columnsFrom": [ + "connection_id" + ], + "tableTo": "integration_connection", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "columnsFrom": [ + "catalog_id" + ], + "tableTo": "mcp_catalog", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "columnsFrom": [ + "task_id", + "session_id" + ], + "tableTo": "task_session", + "columnsTo": [ + "task_id", + "session_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "columnsFrom": [ + "execution_id" + ], + "tableTo": "mcp_execution", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "columnsFrom": [ + "authoring_task_id" + ], + "tableTo": "task", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "mcp_grant_publication": { + "name": "mcp_grant_publication", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "columnsFrom": [ + "target_id" + ], + "tableTo": "review_target", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "columnsFrom": [ + "task_id" + ], + "tableTo": "task", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "columnsFrom": [ + "review_id" + ], + "tableTo": "review", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "columnsFrom": [ + "review_id" + ], + "tableTo": "review", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "columnsFrom": [ + "finding_id" + ], + "tableTo": "review_finding", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "columnsFrom": [ + "router_id", + "model_id" + ], + "tableTo": "router_model", + "columnsTo": [ + "router_id", + "model_id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "columnsFrom": [ + "owner_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "columnsFrom": [ + "template_id" + ], + "tableTo": "spec_template", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "columnsFrom": [ + "published_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "columnsFrom": [ + "author_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "columnsFrom": [ + "author_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "columns": [ + "prompt_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "columnsFrom": [ + "run_id" + ], + "tableTo": "spec_gap_check_run", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "columnsFrom": [ + "disposed_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "columnsFrom": [ + "started_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "columnsFrom": [ + "opened_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "columnsFrom": [ + "resolved_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "columnsFrom": [ + "requested_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "columnsFrom": [ + "settled_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "tableTo": "spec_ticket_draft", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "columnsFrom": [ + "caller_spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "columnsFrom": [ + "actor_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "columnsFrom": [ + "spec_id" + ], + "tableTo": "spec", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "isExpression": true, + "asc": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "isExpression": true, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "source->>'provider' = 'slack'", + "concurrently": false + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "canonical_task_name IS NOT NULL", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_binding_envelope": { + "name": "mcp_binding_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "columnsFrom": [ + "task_id" + ], + "tableTo": "task", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "columnsFrom": [ + "profile_id" + ], + "tableTo": "profile", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "columnsFrom": [ + "registration_id" + ], + "tableTo": "webhook_registration", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/_journal.json b/orchestrator/drizzle/meta/_journal.json index 6c5c3dabb..8b3a32407 100644 --- a/orchestrator/drizzle/meta/_journal.json +++ b/orchestrator/drizzle/meta/_journal.json @@ -771,6 +771,13 @@ "when": 1793609726214, "tag": "0109_mcp_fixed_execute_deadline", "breakpoints": true + }, + { + "idx": 110, + "version": "7", + "when": 1793611948671, + "tag": "0110_mcp_preflight_rejection", + "breakpoints": true } ] } \ No newline at end of file diff --git a/orchestrator/src/__tests__/mcp-effect-driver.test.ts b/orchestrator/src/__tests__/mcp-effect-driver.test.ts new file mode 100644 index 000000000..1dc85fedf --- /dev/null +++ b/orchestrator/src/__tests__/mcp-effect-driver.test.ts @@ -0,0 +1,585 @@ +import { expect, test } from "bun:test"; +import { effectFixture } from "./mcp-effect-fixture.ts"; +import { reviewContext } from "../mcp/journal-review.ts"; + +test("actual worker read in unknown policy preserves raw args, schema lease and redacted outcome once", async () => { + const f = await effectFixture({ eligibility: "unknown" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + expect(call.originalArguments).toBe(f.request.originalArguments); + const prepared = await f.driver.driveOnce(call.id); + expect(prepared.state).toBe("succeeded"); + expect(prepared.result).toMatchObject({ + outcome: "succeeded", + delivery: "available", + output: { isError: false, structuredContent: { echo: "[redacted] | [redacted]" } }, + }); + expect(JSON.stringify(prepared)).not.toContain("known-literal-token"); + expect((await f.command("stats")).calls).toBe(1); + expect(f.leases).toBe(1); + expect(f.events.at(-1)).toBe("read-fence"); + expect((await f.driver.acceptToolRequest(f.request)).id).toBe(call.id); + expect((await f.driver.driveOnce(call.id)).state).toBe("succeeded"); + expect((await f.command("stats")).calls).toBe(1); + await expect( + f.driver.acceptToolRequest({ ...f.request, originalArguments: '{"x":1}' }), + ).rejects.toThrow(); + } finally { + await f.cleanup(); + } +}, 20000); + +test("write schema releases capacity before approval and uses a distinct execute generation", async () => { + const f = await effectFixture({ classification: "write" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + expect((await f.driver.driveOnce(call.id)).state).toBe("waiting_approval"); + const schema = (await f.journal.getAdmission(call.id))!; + expect(schema.state).toBe("released"); + expect(schema.schemaDigest).toBe(call.requestDigest); + expect(f.leases).toBe(0); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); + let executeId = ""; + f.beforeLease(async () => { + executeId = (await f.journal.getAdmission(call.id))!.reservationId; + }); + expect((await f.driver.driveOnce(call.id)).state).toBe("succeeded"); + expect(executeId).not.toBe(schema.reservationId); + expect(f.events.at(-1)).toBe("write-fence"); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await f.cleanup(); + } +}, 20000); + +for (const response of ["image", "error", "malformed", "oversize", "normalized-limit"] as const) + test(`actual worker ${response} retains outcome and delivery without replay`, async () => { + const f = await effectFixture({ classification: "write", response }); + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(call.id); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); + const result = await f.driver.driveOnce(call.id); + expect(result.state).toBe( + response === "error" + ? "failed" + : response === "malformed" || response === "oversize" + ? "outcome_unknown" + : "succeeded", + ); + expect(result.resultUnavailable).toBe( + response === "image" || response === "normalized-limit", + ); + expect(JSON.stringify(result)).not.toContain("known-literal-token"); + if (response === "error") + expect(result.result).toMatchObject({ + output: { isError: true, structuredContent: { echo: "[redacted] | [redacted]" } }, + }); + await f.driver.driveOnce(call.id); + expect((await f.command("stats")).calls).toBe(1); + if (result.state === "outcome_unknown") { + const next = { ...f.request, toolCallId: "call-2" }; + f.requests.set(next.toolCallId, next); + await expect(f.driver.acceptToolRequest(next)).rejects.toThrow("write authority"); + } + } finally { + await f.cleanup(); + } + }, 20000); + +test("plan reads pass while plan and unknown writes deny before schema or secrets", async () => { + for (const classification of ["read", "write"] as const) { + const f = await effectFixture({ classification, eligibility: "plan" }); + try { + if (classification === "read") { + const call = await f.driver.acceptToolRequest(f.request); + expect((await f.driver.driveOnce(call.id)).state).toBe("succeeded"); + } else { + await expect(f.driver.acceptToolRequest(f.request)).rejects.toThrow(); + f.setEligibility("unknown"); + await expect(f.driver.acceptToolRequest(f.request)).rejects.toThrow(); + expect(f.leases).toBe(0); + expect((await f.command("stats")).credentialRequests).toBe(0); + } + } finally { + await f.cleanup(); + } + } +}, 20000); + +test("independent input constraints reject without approval or credentials", async () => { + const f = await effectFixture({ + classification: "write", + constraints: [ + { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: "https://schema.example/constraint", + properties: { x: { maximum: 0 } }, + }, + ], + }); + try { + const call = await f.driver.acceptToolRequest(f.request); + const denied = await f.driver.driveOnce(call.id); + expect(denied.state).toBe("failed"); + expect(denied.intentId).toBeNull(); + expect(denied.result).toMatchObject({ + source: "local_validation", + delivery: "available", + output: { isError: true }, + }); + expect(await f.journal.getAdmission(call.id)).toBeNull(); + expect(await f.journal.pending(f.sessionId, f.actorId, new Date())).toHaveLength(0); + expect(f.leases).toBe(0); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("output schema compiles separately and failed output validation preserves known success", async () => { + const f = await effectFixture({ + outputSchema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + required: ["missing"], + }, + }); + try { + const call = await f.driver.acceptToolRequest(f.request); + const result = await f.driver.driveOnce(call.id); + expect(result.state).toBe("succeeded"); + expect(result.resultUnavailable).toBe(true); + expect(result.result).toMatchObject({ reason: "output_schema" }); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await f.cleanup(); + } +}, 20000); + +test("changed raw bytes and oversized arguments never acquire credentials", async () => { + const f = await effectFixture(); + try { + await expect( + f.driver.acceptToolRequest({ ...f.request, originalArguments: '{"x":1}' }), + ).rejects.toThrow("authority"); + await expect( + f.driver.acceptToolRequest({ + ...f.request, + originalArguments: JSON.stringify({ x: "x".repeat(256 * 1024) }), + }), + ).rejects.toThrow("limit"); + expect(f.leases).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("duplicate concurrent drive crosses one barrier and calls upstream once", async () => { + const f = await effectFixture({ classification: "write" }); + try { + const [a, b] = await Promise.all([ + f.driver.acceptToolRequest(f.request), + f.driver.acceptToolRequest(f.request), + ]); + expect(a.id).toBe(b.id); + expect(a.deadline).toEqual(b.deadline); + await f.driver.driveOnce(a.id); + const approval = await f.journal.requestApproval(a.id, f.deploymentId, new Date()); + await f.journal.decide(a.id, f.actorId, true, reviewContext(a, approval), new Date()); + const results = await Promise.allSettled([f.driver.driveOnce(a.id), f.driver.driveOnce(a.id)]); + expect(results.some((r) => r.status === "fulfilled" && r.value.state === "succeeded")).toBe( + true, + ); + expect((await f.command("stats")).calls).toBe(1); + expect(f.leases).toBe(1); + } finally { + await f.cleanup(); + } +}, 20000); + +test("credential lease rotation preserves frozen binding and redacts the new token", async () => { + const f = await effectFixture(); + try { + const first = await f.driver.acceptToolRequest(f.request); + expect((await f.driver.driveOnce(first.id)).state).toBe("succeeded"); + f.refresh(); + const request = { ...f.request, toolCallId: "call-2" }; + f.requests.set(request.toolCallId, request); + const second = await f.driver.acceptToolRequest(request); + expect(second.bindingHash).toBe(first.bindingHash); + expect(second.requestContext?.resolvedTool.binding).toEqual( + first.requestContext?.resolvedTool.binding, + ); + const result = await f.driver.driveOnce(second.id); + expect(result.state).toBe("succeeded"); + expect(JSON.stringify(result)).not.toContain("refreshed-literal-token"); + expect((await f.command("stats")).calls).toBe(2); + } finally { + await f.cleanup(); + } +}, 20000); + +import { eq } from "drizzle-orm"; +import { integrationConnection, task, taskSession, user } from "../db/schema.ts"; +import { statusMetadata } from "../mcp/credential-store.ts"; +import { makeProfileStore } from "../db/profiles.ts"; + +for (const change of [ + "ban", + "owner", + "principal", + "publication", + "account", + "generation", + "endpoint", + "zone", +] as const) + test(`live ${change} change denies the frozen call before credential lease`, async () => { + const f = await effectFixture(); + try { + const call = await f.driver.acceptToolRequest(f.request); + if (change === "ban") + await f.db.update(user).set({ banned: true }).where(eq(user.id, f.actorId)); + else if (change === "owner") + await f.db.update(task).set({ createdByUserId: null }).where(eq(task.id, f.taskId)); + else if (change === "principal") + await f.db + .update(taskSession) + .set({ integrationPrincipalId: "other" }) + .where(eq(taskSession.sessionId, f.sessionId)); + else if (change === "publication") { + await f.db.update(user).set({ role: "admin" }).where(eq(user.id, f.actorId)); + const profiles = makeProfileStore(f.db); + await profiles.update(f.profileId, (await profiles.get(f.profileId))!, { + actorId: f.actorId, + expectedRevision: 1, + }); + await f.db.update(user).set({ role: "user" }).where(eq(user.id, f.actorId)); + } else if (change === "account") + await f.db + .update(integrationConnection) + .set({ credentialIdentity: "different-account-scope" }) + .where(eq(integrationConnection.id, f.connection.id)); + else if (change === "generation") + await f.db + .update(integrationConnection) + .set({ authGeneration: call.authGeneration + 1 }) + .where(eq(integrationConnection.id, f.connection.id)); + else if (change === "endpoint") + await f.db + .update(integrationConnection) + .set({ config: { ...f.connection.config, endpoint: "https://changed.example.test/mcp" } }) + .where(eq(integrationConnection.id, f.connection.id)); + else f.operatorPolicies[0]!.policyDigest = `sha256:${"a".repeat(64)}`; + // A duplicate returns the original authority even after revocation, but cannot dispatch it. + expect((await f.driver.acceptToolRequest(f.request)).requestDigest).toBe(call.requestDigest); + await expect(f.driver.driveOnce(call.id)).rejects.toThrow(); + expect(f.leases).toBe(0); + expect((await f.command("stats")).credentialRequests).toBe(0); + } finally { + await f.cleanup(); + } + }, 20000); + +test("changed worker boot between barrier and broker identity releases zero credentials", async () => { + const f = await effectFixture(); + try { + const call = await f.driver.acceptToolRequest(f.request); + let changed = false; + f.beforeFence(async () => { + if (!changed && (await f.journal.get(call.id))?.state === "dispatching") { + changed = true; + await f.command("restart"); + } + }); + expect((await f.driver.driveOnce(call.id)).state).toBe("failed"); + expect(changed).toBe(true); + expect(f.leases).toBe(0); + expect((await f.command("stats")).credentialRequests).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("raw coordinator publication policy mismatch is denied before secret leasing", async () => { + const f = await effectFixture(); + try { + const call = await f.driver.acceptToolRequest(f.request); + f.status.context!.policyDigest = "a".repeat(64); + await f.db + .update(integrationConnection) + .set({ mcpCredential: { status: statusMetadata(f.status) } }) + .where(eq(integrationConnection.id, f.connection.id)); + expect((await f.driver.driveOnce(call.id)).state).toBe("failed"); + expect(f.leases).toBe(0); + expect((await f.command("stats")).credentialRequests).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("revocation during credential lease is caught by the second same-generation assertion", async () => { + const f = await effectFixture({ classification: "write" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(call.id); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); + f.afterLease(async () => { + await f.db.update(user).set({ banned: true }).where(eq(user.id, f.actorId)); + }); + expect((await f.driver.driveOnce(call.id)).state).toBe("outcome_unknown"); + expect(f.leases).toBe(1); + expect((await f.command("stats")).credentialRequests).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("final coordinator fence after a lease blocks changed authority", async () => { + const f = await effectFixture(); + try { + const call = await f.driver.acceptToolRequest(f.request); + let postLeaseFences = 0; + f.beforeFence(async () => { + if (f.leases && ++postLeaseFences === 2) f.setFence(false); + }); + expect((await f.driver.driveOnce(call.id)).state).toBe("failed"); + expect(postLeaseFences).toBe(2); + expect((await f.command("stats")).credentialRequests).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("global kill during final fence prevents the immediate send", async () => { + const f = await effectFixture(); + try { + const call = await f.driver.acceptToolRequest(f.request); + let postLeaseFences = 0; + f.beforeFence(async () => { + if (f.leases && ++postLeaseFences === 2) f.setEnabled(false); + }); + expect((await f.driver.driveOnce(call.id)).state).toBe("failed"); + expect((await f.command("stats")).credentialRequests).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("unpublished legacy human grants require explicit administrator publication", async () => { + const f = await effectFixture({ published: false }); + try { + await expect(f.driver.acceptToolRequest(f.request)).rejects.toThrow( + "administrator publication", + ); + expect(f.leases).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("trusted shorter deadline freezes once and duplicate delivery cannot extend it", async () => { + const f = await effectFixture(); + try { + const deadline = new Date(Date.now() + 5000); + const call = await f.driver.acceptToolRequest(f.request, deadline); + expect(call.deadline).toEqual(deadline); + expect( + (await f.driver.acceptToolRequest(f.request, new Date(Date.now() + 60000))).deadline, + ).toEqual(deadline); + expect((await f.driver.driveOnce(call.id)).state).toBe("succeeded"); + } finally { + await f.cleanup(); + } +}, 20000); + +test("an unresolved write blocks new writes but permits the separately reviewed read", async () => { + const f = await effectFixture({ classification: "write", response: "malformed" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(call.id); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); + expect((await f.driver.driveOnce(call.id)).state).toBe("outcome_unknown"); + const readName = f.snapshot.immutableBindings[0]!.tools.find( + (tool) => tool.tool.classification === "read", + )!.name; + const readRequest = { ...f.request, toolCallId: "read-after-unknown", modelToolName: readName }; + f.requests.set(readRequest.toolCallId, readRequest); + f.setEligibility("plan"); + const read = await f.driver.acceptToolRequest(readRequest); + // The fixture still returns malformed output. It proves admission and one read call. + await f.driver.driveOnce(read.id); + expect((await f.command("stats")).calls).toBe(2); + expect(f.leases).toBe(2); + } finally { + await f.cleanup(); + } +}, 20000); + +test("cancel after provider send leaves an unknown write that cannot replay", async () => { + const f = await effectFixture({ classification: "write", response: "hold" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(call.id); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); + const controller = new AbortController(); + const pending = f.driver.driveOnce(call.id, controller.signal); + const until = Date.now() + 5000; + while (!(await f.command("stats")).calls && Date.now() < until) + await new Promise((resolve) => setTimeout(resolve, 10)); + expect((await f.command("stats")).calls).toBe(1); + await f.journal.cancel(call.id, f.actorId, new Date()); + controller.abort(); + expect((await pending).state).toBe("outcome_unknown"); + await f.driver.driveOnce(call.id); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await f.cleanup(); + } +}, 20000); + +import { automation, automationRun, automationSession } from "../db/schema.ts"; + +test("published automation grant uses exact live automation linkage without human approval", async () => { + const f = await effectFixture({ classification: "write" }); + const automationId = crypto.randomUUID(), + runId = crypto.randomUUID(); + try { + await f.db + .insert(automation) + .values({ id: automationId, name: "Fixture automation", createdByUserId: f.actorId }); + await f.db.insert(automationRun).values({ + id: runId, + automationId, + taskId: f.taskId, + sessionId: f.sessionId, + status: "running", + trigger: { source: "manual", receivedAt: new Date().toISOString() }, + }); + await f.db + .insert(automationSession) + .values({ sessionId: f.sessionId, runId, blockId: "start" }); + await f.db + .update(taskSession) + .set({ integrationPrincipalId: `automation:${automationId}` }) + .where(eq(taskSession.sessionId, f.sessionId)); + await f.db.update(task).set({ createdByUserId: null }).where(eq(task.id, f.taskId)); + const call = await f.driver.acceptToolRequest(f.request); + expect(call.authorizationKind).toBe("automation"); + expect((await f.driver.driveOnce(call.id)).state).toBe("succeeded"); + const request = { ...f.request, toolCallId: "automation-next" }; + f.requests.set(request.toolCallId, request); + const next = await f.driver.acceptToolRequest(request); + await f.db.update(automation).set({ enabled: false }).where(eq(automation.id, automationId)); + await expect(f.driver.driveOnce(next.id)).rejects.toThrow("automation"); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await f.db.delete(automation).where(eq(automation.id, automationId)); + await f.cleanup(); + } +}, 20000); + +test("live admin role is required when the immutable principal is not the current owner", async () => { + const f = await effectFixture(); + const adminId = crypto.randomUUID(); + try { + await f.db.insert(user).values({ + id: adminId, + name: "Fixture admin", + email: `${adminId}@example.test`, + role: "admin", + createdAt: new Date(), + updatedAt: new Date(), + }); + await f.db + .update(taskSession) + .set({ integrationPrincipalId: adminId }) + .where(eq(taskSession.sessionId, f.sessionId)); + const call = await f.driver.acceptToolRequest(f.request); + await f.db.update(user).set({ role: "user" }).where(eq(user.id, adminId)); + await expect(f.driver.driveOnce(call.id)).rejects.toThrow(); + expect(f.leases).toBe(0); + } finally { + await f.cleanup(); + await f.db.delete(user).where(eq(user.id, adminId)); + } +}, 20000); + +import { create } from "@bufbuild/protobuf"; +import { McpWritePolicyRefSchema } from "../gen/engram/app/v1/session_pb.ts"; +import { bindingSnapshotColumns, readLiveBindingAuthority } from "../mcp/binding-store.ts"; +import { compileSessionBindings } from "../mcp/session-bindings.ts"; + +test("live ancestor restriction denies child execution", async () => { + const f = await effectFixture(); + const parentId = crypto.randomUUID(), + parentTaskId = crypto.randomUUID(); + try { + const [leaf] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + await f.db.insert(task).values({ id: parentTaskId, type: "chat", createdByUserId: f.actorId }); + await f.db.insert(taskSession).values({ + taskId: parentTaskId, + sessionId: parentId, + profileId: f.profileId, + integrationPrincipalId: f.actorId, + ...bindingSnapshotColumns(f.snapshot, leaf!.integrationConnections!), + }); + const authority = await readLiveBindingAuthority(f.profileId, f.operatorPolicies, f.db); + const child = compileSessionBindings({ + launchPolicy: { profileId: f.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + requestedGrants: authority.profileGrants, + parentAuthority: { sessionId: parentId, snapshot: f.snapshot }, + }); + await f.db.update(task).set({ spawningSessionId: parentId }).where(eq(task.id, f.taskId)); + await f.db + .update(taskSession) + .set(bindingSnapshotColumns(child, leaf!.integrationConnections!)) + .where(eq(taskSession.sessionId, f.sessionId)); + const parentRef = create(McpWritePolicyRefSchema, { + sessionId: parentId, + bindingEpoch: 0n, + policyRevision: 1n, + }); + f.policies.set(parentId, { ref: parentRef, ancestorPolicyRefs: [] }); + f.policies.get(f.sessionId)!.ancestorPolicyRefs = [parentRef]; + await f.resolver.readCurrent(f.sessionId); + const call = await f.driver.acceptToolRequest(f.request); + await f.db.update(task).set({ status: "failed" }).where(eq(task.id, parentTaskId)); + await expect(f.driver.driveOnce(call.id)).rejects.toThrow(); + expect(f.leases).toBe(0); + } finally { + await f.db.delete(task).where(eq(task.id, f.taskId)); + await f.db.delete(task).where(eq(task.id, parentTaskId)); + await f.cleanup(); + } +}, 20000); + +test("credential assertion rejects an altered execution reservation after leasing", async () => { + const f = await effectFixture(); + try { + const call = await f.driver.acceptToolRequest(f.request); + const { mcpAdmission } = await import("../db/schema.ts"); + f.afterLease(async () => { + await f.db + .update(mcpAdmission) + .set({ reservationId: crypto.randomUUID() }) + .where(eq(mcpAdmission.executionId, call.id)); + }); + expect((await f.driver.driveOnce(call.id)).state).toBe("failed"); + expect(f.leases).toBe(1); + expect((await f.command("stats")).credentialRequests).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-effect-fixture.ts b/orchestrator/src/__tests__/mcp-effect-fixture.ts new file mode 100644 index 000000000..80fe2f08c --- /dev/null +++ b/orchestrator/src/__tests__/mcp-effect-fixture.ts @@ -0,0 +1,337 @@ +import { expect } from "bun:test"; +import { spawn } from "node:child_process"; +import { createInterface } from "node:readline"; +import { once } from "node:events"; +import { create } from "@bufbuild/protobuf"; +import { createClient, createRouterTransport } from "@connectrpc/connect"; +import { eq } from "drizzle-orm"; +import { workerTls } from "../../../mcp-worker/test/fixtures/worker-tls.ts"; +import type { WorkerIdentity } from "../../../mcp-worker/src/contracts.ts"; +import { reviewFixture } from "./mcp-review-fixture.ts"; +import { makeProfileStore } from "../db/profiles.ts"; +import { user, taskSession, integrationConnection } from "../db/schema.ts"; +import { compileSessionBindings } from "../mcp/session-bindings.ts"; +import { + bindingSnapshotColumns, + readLiveBindingAuthority, + makeStoredSessionBindingResolver, +} from "../mcp/binding-store.ts"; +import { + CredentialControlService, + CredentialKind, + CredentialStatusSchema, +} from "../gen/engram/mcp/v1/control_pb.ts"; +import { SessionPolicyService } from "../gen/engram/mcp/v1/session_policy_pb.ts"; +import type { McpWritePolicyRef } from "../gen/engram/app/v1/session_pb.ts"; +import { statusMetadata } from "../mcp/credential-store.ts"; +import { unwrapCredentialControlClient } from "../mcp/control-client.ts"; +import { makeMcpEffectDriver } from "../mcp/effect-driver.ts"; + +export async function effectFixture( + options: { + classification?: "read" | "write"; + response?: string; + constraints?: Record[]; + outputSchema?: Record; + eligibility?: string; + published?: boolean; + } = {}, +) { + const tls = await workerTls("localhost"); + const child = spawn( + process.env.MCP_WORKER_TEST_NODE ?? "node", + [new URL("../../../mcp-worker/test/fixtures/effect-worker.ts", import.meta.url).pathname], + { stdio: ["pipe", "pipe", "pipe"], env: { PATH: process.env.PATH ?? "" } }, + ); + const output = createInterface({ input: child.stdout }), + lines = output[Symbol.asyncIterator](); + let stderr = ""; + child.stderr.on("data", (data) => { + stderr += String(data); + }); + child.stdin.write(`${JSON.stringify({ tls, response: options.response })}\n`); + async function next() { + const line = await lines.next(); + if (line.done) throw Error(`fixture exited: ${stderr}`); + return JSON.parse(line.value); + } + const ready: { port: number; identity: WorkerIdentity; node: string } = await next(); + expect(ready.node.startsWith("24.")).toBe(true); + const f = await reviewFixture(options.constraints, options.classification === "write" ? 2 : 1); + try { + const classification = options.classification ?? "read"; + const tools = f.authority.profileGrants.map((grant) => ({ + upstreamName: grant.mcp!.upstreamName, + description: "Fixture operation", + inputSchema: { type: "object", properties: { x: { type: "integer" } }, required: ["x"] }, + ...(options.outputSchema ? { outputSchema: options.outputSchema } : {}), + })); + const current = (await f.connections.get(f.connection.id))!; + const draft = await f.repository.saveDraft( + current.id, + current.mcp!, + tools, + f.actorId, + new Date(), + ); + await f.repository.publish( + current.id, + draft.id, + 1, + tools.map((tool, index) => ({ + ...tool, + classification: index === 0 ? classification : "read", + enabled: true, + })), + f.actorId, + new Date(), + ); + await f.repository.setEnabled(current.id, 1, true, new Date()); + const profiles = makeProfileStore(f.db); + await f.db.update(user).set({ role: "admin" }).where(eq(user.id, f.actorId)); + if (options.published !== false) + await profiles.update(f.profileId, (await profiles.get(f.profileId))!, { + actorId: f.actorId, + expectedRevision: 0, + }); + await f.db.update(user).set({ role: "user" }).where(eq(user.id, f.actorId)); + const operatorPolicies = [ + { + zoneId: ready.identity.zoneId, + policyRevision: ready.identity.policyRevision, + policyDigest: `sha256:${ready.identity.policyDigest}`, + authorities: ["tools.example.test"], + aliases: [], + }, + ]; + const authority = await readLiveBindingAuthority(f.profileId, operatorPolicies, f.db); + const snapshot = compileSessionBindings({ + launchPolicy: { profileId: f.profileId, integrationGrants: authority.profileGrants }, + liveAuthority: authority, + bindingEpoch: 0, + }); + await f.db + .update(taskSession) + .set( + bindingSnapshotColumns(snapshot, [ + { + id: current.id, + alias: current.alias, + provider: "mcp", + displayName: current.displayName, + config: current.config, + }, + ]), + ) + .where(eq(taskSession.sessionId, f.sessionId)); + const binding = snapshot.immutableBindings[0]!; + const request = { + sessionId: f.sessionId, + runId: "run-1", + toolCallId: "call-1", + modelToolName: binding.tools.find((bound) => bound.tool.upstreamName === "Write.Record")! + .name, + originalArguments: '{ "x": 1 }', + }; + const requests = new Map([[request.toolCallId, request]]); + const events: string[] = []; + let eligibility = options.eligibility ?? "write_eligible", + fenceCurrent = true, + token = "known-literal-token", + leases = 0; + let enabled = true; + let beforeFence: (() => Promise) | undefined; + let beforeLease: (() => Promise) | undefined; + let afterLease: (() => Promise) | undefined; + const policies = new Map< + string, + { ref: McpWritePolicyRef; ancestorPolicyRefs: McpWritePolicyRef[] } + >([ + [ + f.sessionId, + { + ref: f.input.requestPolicyContext.acceptedPolicyRef, + ancestorPolicyRefs: f.input.requestPolicyContext.ancestorPolicyRefs, + }, + ], + ]); + const policy = createClient( + SessionPolicyService, + createRouterTransport((router) => + router.service(SessionPolicyService, { + async readWritePolicy(req) { + const refs = policies.get(req.sessionId)!; + return { + state: { + policy: { + current: true, + initialized: eligibility !== "unknown", + writeEligibility: eligibility, + ...refs, + }, + }, + }; + }, + async validateExecutionIdentity(req) { + events.push("read-fence"); + await beforeFence?.(); + const original = requests.get(req.toolCallId); + return { + protocol: 1, + status: + fenceCurrent && + original?.originalArguments === req.originalArguments && + original?.modelToolName === req.modelToolName + ? "current" + : "stale", + }; + }, + async validateDispatchFence(req) { + events.push("write-fence"); + await beforeFence?.(); + const original = requests.get(req.toolCallId); + return { + protocol: 1, + status: + fenceCurrent && + eligibility === "write_eligible" && + original?.originalArguments === req.originalArguments && + original?.modelToolName === req.modelToolName + ? "current" + : "stale", + }; + }, + }), + ), + ); + const status = create(CredentialStatusSchema, { + protocol: 1, + status: "connected", + version: 1n, + context: { + connectionId: current.id, + configRevision: BigInt(binding.configRevision), + authorizationGeneration: BigInt(binding.authGeneration), + credentialIdentity: binding.credentialIdentity, + resource: { endpoint: binding.config.endpoint, zoneId: binding.config.zone }, + policyDigest: ready.identity.policyDigest, + policyRevision: ready.identity.policyRevision, + }, + reference: { + kind: CredentialKind.API_KEY, + connectionId: current.id, + name: "mcp.test.key", + generation: 1n, + }, + }); + await f.db + .update(integrationConnection) + .set({ mcpCredential: { status: statusMetadata(status) } }) + .where(eq(integrationConnection.id, current.id)); + const control = unwrapCredentialControlClient( + createClient( + CredentialControlService, + createRouterTransport((router) => + router.service(CredentialControlService, { + async lease(req) { + events.push("lease"); + await beforeLease?.(); + leases++; + await afterLease?.(); + return { + protocol: 1, + intentId: req.intentId, + expiresAt: req.expiresAt, + operationExpiresAt: req.operationExpiresAt, + header: "authorization", + value: `Bearer ${token}`, + }; + }, + }), + ), + ), + ); + // Operator policy, host enforcement ACK, and coordinator history are fixture trusted boundaries. + const resolver = makeStoredSessionBindingResolver( + { + control: policy, + readOperatorPolicies: async () => operatorPolicies, + enforceManagedDeny: async (expected) => expected, + }, + f.db, + ); + const driver = makeMcpEffectDriver({ + db: f.db, + deploymentId: f.deploymentId, + control, + policy, + resolver, + workerTls: () => ({ + ...tls, + url: `https://127.0.0.1:${ready.port}`, + serverFingerprint: tls.fingerprint, + }), + enabled: () => enabled, + now: () => new Date(), + }); + return { + ...f, + driver, + resolver, + request, + requests, + snapshot, + status, + events, + policies, + operatorPolicies, + setEnabled(value: boolean) { + enabled = value; + }, + beforeFence(fn: () => Promise) { + beforeFence = fn; + }, + get leases() { + return leases; + }, + setEligibility(value: string) { + eligibility = value; + }, + setFence(value: boolean) { + fenceCurrent = value; + }, + beforeLease(fn: () => Promise) { + beforeLease = fn; + }, + afterLease(fn: () => Promise) { + afterLease = fn; + }, + refresh() { + token = "refreshed-literal-token"; + }, + async command(command: string) { + child.stdin.write(`${command}\n`); + return next(); + }, + async cleanup() { + try { + await f.cleanup(); + } finally { + const exited = once(child, "exit"); + child.stdin.end("close\n"); + await exited; + output.close(); + expect(stderr).toBe(""); + } + }, + }; + } catch (error) { + try { + await f.cleanup(); + } finally { + child.kill(); + output.close(); + } + throw error; + } +} diff --git a/orchestrator/src/__tests__/mcp-effect-result.test.ts b/orchestrator/src/__tests__/mcp-effect-result.test.ts new file mode 100644 index 000000000..c7af285fd --- /dev/null +++ b/orchestrator/src/__tests__/mcp-effect-result.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from "bun:test"; +import { verifiedExecutionResult } from "../mcp/effect-driver.ts"; + +test("verified result keeps upstream outcome and delivery separate", () => { + expect( + verifiedExecutionResult({ + outcome: "succeeded", + delivery: "unavailable", + reason: "output_schema", + }), + ).toEqual({ + state: "succeeded", + result: { outcome: "succeeded", delivery: "unavailable", reason: "output_schema" }, + resultUnavailable: true, + }); + const error = { + outcome: "failed", + delivery: "available", + output: { + content: [{ type: "text", text: "rejected" }], + structuredContent: { code: 1 }, + isError: true, + }, + }; + expect(verifiedExecutionResult(error)).toEqual({ + state: "failed", + result: error, + resultUnavailable: false, + }); + expect( + verifiedExecutionResult({ + outcome: "outcome_unknown", + delivery: "unavailable", + reason: "upstream_failure", + }).state, + ).toBe("outcome_unknown"); +}); + +test("malformed and contradictory outcomes never establish success", () => { + for (const value of [ + null, + {}, + { outcome: "succeeded" }, + { + outcome: "succeeded", + delivery: "available", + output: { content: [{ type: "image", data: "AA==" }] }, + }, + { outcome: "succeeded", delivery: "available", output: { content: [], isError: true } }, + { outcome: "outcome_unknown", delivery: "available", output: { content: [] } }, + { outcome: "failed", delivery: "unavailable", reason: "invented" }, + { outcome: "succeeded", delivery: "unavailable", reason: "output_schema", secret: "untrusted" }, + ]) { + expect(() => verifiedExecutionResult(value)).toThrow(); + } +}); diff --git a/orchestrator/src/__tests__/mcp-preflight-rejection.test.ts b/orchestrator/src/__tests__/mcp-preflight-rejection.test.ts new file mode 100644 index 000000000..4e54ab0ca --- /dev/null +++ b/orchestrator/src/__tests__/mcp-preflight-rejection.test.ts @@ -0,0 +1,109 @@ +import { expect, test } from "bun:test"; +import { schemaVerifiedFixture } from "./mcp-review-fixture.ts"; +import { reviewContext } from "../mcp/journal-review.ts"; + +test("preflight rejection cannot release another generation or a committed dispatch", async () => { + const f = await schemaVerifiedFixture(); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const approved = ( + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), f.now) + ).call; + await f.journal.admit(call.id, f.deploymentId, f.now); + const lease = (await f.journal.getAdmission(call.id))!; + const rejection = { + requestDigest: call.requestDigest, + version: approved.version, + reservation: { reservationId: lease.reservationId, expiresAt: lease.expiresAt }, + }; + await expect( + f.journal.rejectBeforeDispatch( + call.id, + { ...rejection, reservation: { ...rejection.reservation, reservationId: "stale" } }, + f.now, + ), + ).rejects.toThrow(); + expect((await f.journal.getAdmission(call.id))!.reservationId).toBe(lease.reservationId); + const proof = { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: approved.version, + reservationId: lease.reservationId, + reservationExpiresAt: lease.expiresAt, + }; + const results = await Promise.allSettled([ + f.journal.dispatch( + call.id, + { bootId: "boot", intentId: crypto.randomUUID(), expiresAt: lease.expiresAt }, + f.now, + proof, + ), + f.journal.rejectBeforeDispatch(call.id, rejection, f.now), + ]); + expect(results.filter((r) => r.status === "fulfilled")).toHaveLength(1); + const row = (await f.journal.get(call.id))!; + if (row.state === "dispatching") + expect((await f.journal.getAdmission(call.id))!.reservationId).toBe(lease.reservationId); + else { + expect(row.state).toBe("failed"); + expect(row.intentId).toBeNull(); + expect(await f.journal.getAdmission(call.id)).toBeNull(); + } + } finally { + await f.cleanup(); + } +}); + +import { persistVerifiedOutcome } from "../mcp/effect-driver.ts"; +test("a verified late success survives cancellation between result read and result commit", async () => { + const f = await schemaVerifiedFixture(); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const approved = ( + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), f.now) + ).call; + await f.journal.admit(call.id, f.deploymentId, f.now); + const lease = (await f.journal.getAdmission(call.id))!; + const dispatched = await f.journal.dispatch( + call.id, + { bootId: "boot", intentId: crypto.randomUUID(), expiresAt: lease.expiresAt }, + f.now, + { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: approved.version, + reservationId: lease.reservationId, + reservationExpiresAt: lease.expiresAt, + }, + ); + let commits = 0; + const result = await persistVerifiedOutcome( + { + ...f.journal, + async finish(...args) { + if (++commits === 1) await f.journal.cancel(call.id, f.actorId, f.now); + return f.journal.finish(...args); + }, + }, + dispatched, + { + state: "succeeded", + result: { outcome: "succeeded", delivery: "available", output: { content: [] } }, + resultUnavailable: false, + }, + () => f.now, + ); + expect(result.state).toBe("succeeded"); + expect(result.intentId).toBe(dispatched.intentId); + expect(commits).toBe(2); + expect(await f.journal.getAdmission(call.id)).toBeNull(); + } finally { + await f.cleanup(); + } +}); diff --git a/orchestrator/src/__tests__/mcp-rpc.test.ts b/orchestrator/src/__tests__/mcp-rpc.test.ts index 4df7b22af..de7ba1fe4 100644 --- a/orchestrator/src/__tests__/mcp-rpc.test.ts +++ b/orchestrator/src/__tests__/mcp-rpc.test.ts @@ -76,6 +76,7 @@ function services(): McpServices { admit: unavailable, admitSchema: unavailable, finishSchema: unavailable, + rejectBeforeDispatch: unavailable, assertDispatchCurrent: unavailable, dispatch: unavailable, finish: unavailable, diff --git a/orchestrator/src/mcp/effect-driver.ts b/orchestrator/src/mcp/effect-driver.ts new file mode 100644 index 000000000..acea216f8 --- /dev/null +++ b/orchestrator/src/mcp/effect-driver.ts @@ -0,0 +1,513 @@ +import { fromJson } from "@bufbuild/protobuf"; +import { and, eq } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { integrationConnection, mcpExecution, taskSession } from "../db/schema.ts"; +import { CredentialStatusSchema } from "../gen/engram/mcp/v1/control_pb.ts"; +import { digest } from "../../../mcp-worker/src/index.ts"; +import type { + WorkerIdentity, + WorkerIntent, + WorkerOperation, +} from "../../../mcp-worker/src/contracts.ts"; +import type { + CredentialControlClient, + CredentialControlTls, + SessionPolicyControlClient, +} from "./control-client.ts"; +import { makeCredentialBroker } from "./credential-broker.ts"; +import { canonicalDigest, canonicalJson } from "./hash.ts"; +import { makeMcpJournal, type McpLiveDispatchAuthorization } from "./journal.ts"; +import { requestPolicyContext } from "./journal-review.ts"; +import { MCP_LIMITS } from "./limits.ts"; +import { lockRuntimeAuthority } from "./runtime-authority.ts"; +import { makeSessionWritePolicy, type RequestPolicyContext } from "./session-policy.ts"; +import type { SessionBindingResolver, ResolvedBoundTool } from "./session-resolver.ts"; +import type { McpSessionBindingSnapshot } from "./session-bindings.ts"; +import { makeResourceWorkerClient } from "./worker-client.ts"; +import { toWorkerBindingHash } from "./worker-encoding.ts"; + +type Call = typeof mcpExecution.$inferSelect; +type Lease = NonNullable["getAdmission"]>>>; +export interface McpToolRequest { + sessionId: string; + runId: string; + toolCallId: string; + modelToolName: string; + /** Exact first coordinator tool_call_requested.args_json, not parsed and serialized again. */ + originalArguments: string; +} +class InvalidArguments extends Error { + constructor(readonly lease: Lease) { + super("MCP arguments do not satisfy approved schemas"); + } +} +function object(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} +function argumentsValue(raw: string): Record { + if (Buffer.byteLength(raw) > MCP_LIMITS.argumentBytes) throw Error("MCP arguments exceed limit"); + const value: unknown = JSON.parse(raw); + if (!object(value)) throw Error("MCP arguments must be an object"); + return value; +} +function publication(snapshot: McpSessionBindingSnapshot) { + const value = snapshot.profilePublication; + if ( + !value || + !Number.isSafeInteger(value.revision) || + value.revision < 1 || + !value.actorId || + value.grantsHash !== snapshot.profileGrantsHash + ) + throw Error("MCP profile grants require administrator publication"); +} + +/** The authenticated worker envelope is verified by the client and broker first. */ +export function verifiedExecutionResult(value: unknown): { + state: "succeeded" | "failed" | "outcome_unknown"; + result: Record; + resultUnavailable: boolean; +} { + if ( + !object(value) || + Object.keys(value).some((key) => !["outcome", "delivery", "output", "reason"].includes(key)) || + !["succeeded", "failed", "outcome_unknown"].includes(String(value.outcome)) || + !["available", "unavailable"].includes(String(value.delivery)) + ) + throw Error("MCP result invalid"); + const state = value.outcome; + if (state !== "succeeded" && state !== "failed" && state !== "outcome_unknown") + throw Error("MCP result invalid"); + if (value.delivery === "available") { + const output = value.output; + if ( + state === "outcome_unknown" || + value.reason !== undefined || + !object(output) || + Object.keys(output).some( + (key) => !["content", "structuredContent", "isError"].includes(key), + ) || + !Array.isArray(output.content) || + output.content.some( + (item: unknown) => + !object(item) || + Object.keys(item).some((key) => !["type", "text"].includes(key)) || + item.type !== "text" || + typeof item.text !== "string", + ) || + (output.isError !== undefined && typeof output.isError !== "boolean") || + (state === "failed" ? output.isError !== true : output.isError === true) + ) + throw Error("MCP result invalid"); + if (Buffer.byteLength(canonicalJson(value)) > MCP_LIMITS.resultBytes) { + return { + state, + result: { outcome: state, delivery: "unavailable", reason: "result_limit" }, + resultUnavailable: state === "succeeded", + }; + } + } else if ( + value.output !== undefined || + !["unsupported_content", "result_limit", "output_schema", "upstream_failure"].includes( + String(value.reason), + ) || + (state === "outcome_unknown" && value.reason !== "upstream_failure") + ) + throw Error("MCP result invalid"); + return { + state, + result: structuredClone(value), + resultUnavailable: state === "succeeded" && value.delivery === "unavailable", + }; +} + +/** Record verified truth independently of guest delivery and local cancellation. */ +export async function persistVerifiedOutcome( + journal: ReturnType, + dispatched: Call, + outcome: ReturnType, + now: () => Date, +): Promise { + // Cancellation can change dispatching to unknown once. Only the result CAS is retried. + for (let attempt = 0; attempt < 2; attempt++) { + const latest = await journal.get(dispatched.id); + if (!latest || latest.intentId !== dispatched.intentId) + throw Error("MCP result identity changed"); + if (!["dispatching", "outcome_unknown"].includes(latest.state)) return latest; + if (latest.state === "outcome_unknown" && outcome.state === "outcome_unknown") return latest; + try { + return await journal.finish( + dispatched.id, + latest.version, + outcome.state, + outcome.result, + outcome.resultUnavailable, + now(), + ); + } catch (error) { + if (attempt === 1) throw error; + } + } + throw Error("MCP result commit unavailable"); +} + +/** Trusted effect driver. Call directly, never inside a replayable DBOS/SQL step. + * Task 6 supplies operator/host enforcement and trusted zone TLS configuration. */ +export function makeMcpEffectDriver(deps: { + db?: ReturnType; + deploymentId: string; + control: CredentialControlClient; + policy: SessionPolicyControlClient; + resolver: SessionBindingResolver; + workerTls(zoneId: string): CredentialControlTls; + /** Fresh global and zone kill switches. Missing configuration stays disabled. */ + enabled?(zoneId: string): boolean; + now(): Date; +}) { + const db = deps.db ?? getDb(), + journal = makeMcpJournal(db), + policy = makeSessionWritePolicy(deps.policy); + function enabled(resolved: ResolvedBoundTool, signal?: AbortSignal) { + signal?.throwIfAborted(); + if (!deps.enabled?.(resolved.binding.config.zone)) throw Error("external MCP is disabled"); + } + async function lookup(input: McpToolRequest) { + const [call] = await db + .select() + .from(mcpExecution) + .where( + and( + eq(mcpExecution.sessionId, input.sessionId), + eq(mcpExecution.toolCallId, input.toolCallId), + ), + ); + if ( + call && + (call.runId !== input.runId || + call.originalArguments !== input.originalArguments || + call.requestContext?.resolvedTool.modelToolName !== input.modelToolName) + ) + throw Error("MCP call identity changed"); + return call; + } + async function fence(context: RequestPolicyContext, resolved: ResolvedBoundTool, raw: string) { + const expected = { modelToolName: resolved.modelToolName, originalArguments: raw }; + const status = await (resolved.reviewedTool.classification === "write" + ? policy.validateDispatchFence(context, expected) + : policy.validateExecutionIdentity(context, expected)); + if (status !== "current") throw Error("MCP execution authority changed"); + } + async function current(call: Call, signal?: AbortSignal) { + const resolved = call.requestContext?.resolvedTool; + if (!resolved || call.originalArguments === null || call.deadline <= deps.now()) + throw Error("MCP request unavailable"); + enabled(resolved, signal); + const snapshot = await deps.resolver.readCurrent(call.sessionId); + publication(snapshot); + if (snapshot.bindingHash !== call.bindingHash) throw Error("MCP binding changed"); + await db.transaction((tx) => lockRuntimeAuthority(tx, call, "share")); + if ( + call.classification === "write" && + (await journal.hasUnknown(call.sessionId, call.connectionId)) + ) + throw Error("unknown write outcome blocks this connection"); + await fence(requestPolicyContext(call.requestContext!), resolved, call.originalArguments); + enabled(resolved, signal); + return resolved; + } + function client(resolved: ResolvedBoundTool, signal?: AbortSignal) { + return makeResourceWorkerClient({ + tls: deps.workerTls(resolved.binding.config.zone), + policy: resolved.binding.operatorPolicy, + signal, + }); + } + function intent( + call: Call, + lease: Lease, + identity: WorkerIdentity, + operation: WorkerOperation, + ): Omit { + const bound = call.requestContext!.resolvedTool.binding; + return { + id: crypto.randomUUID(), + bootId: identity.bootId, + zoneId: identity.zoneId, + policyDigest: identity.policyDigest, + connectionId: call.connectionId, + connectionRevision: call.configRevision, + credentialIdentity: bound.credentialIdentity, + scope: { + kind: "execution", + sessionId: call.sessionId, + runId: call.runId, + toolCallId: call.toolCallId, + bindingHash: toWorkerBindingHash(call.bindingHash), + }, + operation: operation.kind, + destination: "endpoint" in operation ? operation.endpoint : "", + requestDigest: call.requestDigest, + expiresAt: lease.expiresAt.getTime(), + lease: { + id: lease.reservationId, + deploymentId: lease.deploymentId, + expiresAt: lease.expiresAt.getTime(), + }, + }; + } + async function schema(call: Call, resolved: ResolvedBoundTool, signal?: AbortSignal) { + if ((await journal.getAdmission(call.id))?.schemaDigest === call.requestDigest) return true; + const operations: Extract[] = [ + ...(resolved.reviewedTool.outputSchema + ? [{ kind: "validateSchemas" as const, documents: [resolved.reviewedTool.outputSchema] }] + : []), + { + kind: "validateSchemas", + documents: [ + resolved.reviewedTool.inputSchema, + ...resolved.effectiveGrant.argumentConstraintDocuments, + ], + value: argumentsValue(call.originalArguments!), + }, + ]; + for (let i = 0; i < operations.length; i++) { + if ((await journal.admitSchema(call.id, deps.deploymentId, deps.now())) === "queued") + return false; + const lease = (await journal.getAdmission(call.id))!; + const worker = client(resolved, signal); + let success = false; + try { + await current(call, signal); + const identity = await worker.identity(), + operation = operations[i]!; + const response = await worker.dispatch({ + protocol: 1, + intent: { + ...intent(call, lease, identity, operation), + operationDigest: digest(operation), + }, + operation, + }); + if (response.ok && response.result === false) throw new InvalidArguments(lease); + if (!response.ok || response.result !== true) throw Error("MCP schema validation failed"); + success = true; + } finally { + worker.close(); + // Compile-only success releases capacity without claiming final argument proof. + await journal.finishSchema( + call.id, + lease.reservationId, + lease.expiresAt, + call.requestDigest, + success && i === operations.length - 1, + deps.now(), + ); + } + } + return true; + } + return { + async acceptToolRequest(input: McpToolRequest, trustedDeadline?: Date) { + input = structuredClone(input); + const existing = await lookup(input); + if (existing) return existing; + const parsed = argumentsValue(input.originalArguments); + const resolved = await deps.resolver.resolveBoundTool(input); + if (!resolved) throw Error("MCP tool unavailable"); + enabled(resolved); + const snapshot = await deps.resolver.readCurrent(input.sessionId); + publication(snapshot); + const currentPolicy = await policy.readForRequest(input); + if (!currentPolicy.current || !currentPolicy.ref) throw Error("MCP session unavailable"); + const context: RequestPolicyContext = { + sessionId: input.sessionId, + runId: input.runId, + toolCallId: input.toolCallId, + acceptedPolicyRef: currentPolicy.ref, + acceptedWriteEligibility: currentPolicy.writeEligibility, + ancestorPolicyRefs: currentPolicy.ancestorPolicyRefs, + }; + await fence(context, resolved, input.originalArguments); + const [session] = await db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, input.sessionId)); + if (!session?.integrationPrincipalId) throw Error("MCP principal unavailable"); + const write = resolved.reviewedTool.classification === "write"; + if ( + write && + (context.acceptedWriteEligibility !== "write_eligible" || + (await journal.hasUnknown(input.sessionId, resolved.binding.connectionId))) + ) + throw Error("MCP write authority unavailable"); + const authorization = !write + ? { kind: "read" as const } + : session.integrationPrincipalId.startsWith("automation:") + ? { kind: "automation" as const, grant: resolved.effectiveGrant } + : { kind: "approval" as const }; + const now = deps.now(); + // Both phase queues and approval share this first immutable outer cap. + const cap = + now.getTime() + + (authorization.kind === "approval" ? MCP_LIMITS.approvalMs : 0) + + MCP_LIMITS.queueMs + + MCP_LIMITS.maxToolMs; + const deadline = new Date(Math.min(cap, trustedDeadline?.getTime() ?? cap)); + if (!Number.isFinite(deadline.getTime()) || deadline <= now) + throw Error("MCP request deadline expired"); + let call: Call; + try { + call = await journal.create( + { + taskId: session.taskId, + ...input, + principalId: session.integrationPrincipalId, + connectionId: resolved.binding.connectionId, + catalogId: resolved.binding.catalogId, + bindingHash: resolved.bindingHash, + argumentHash: canonicalDigest(parsed), + resolvedTool: resolved, + requestPolicyContext: context, + upstreamName: resolved.reviewedTool.upstreamName, + configRevision: resolved.binding.configRevision, + authGeneration: resolved.binding.authGeneration, + classification: resolved.reviewedTool.classification, + deadline, + }, + authorization, + now, + ); + } catch (error) { + // A concurrent first acceptance owns the deadline and immutable context. + const winner = await lookup(input); + if (winner) return winner; + throw error; + } + await current(call); + return call; + }, + async driveOnce(id: string, signal?: AbortSignal): Promise { + let call = await journal.get(id); + if (!call) throw Error("MCP call unavailable"); + if (!["waiting_approval", "authorized"].includes(call.state)) return call; + const resolved = await current(call, signal); + try { + if (!(await schema(call, resolved, signal))) return (await journal.get(id))!; + } catch (error) { + if (!(error instanceof InvalidArguments)) throw error; + return journal.rejectBeforeDispatch( + id, + { + requestDigest: call.requestDigest, + version: call.version, + reservation: { + reservationId: error.lease.reservationId, + expiresAt: error.lease.expiresAt, + }, + }, + deps.now(), + ); + } + if (call.state === "waiting_approval") { + await journal.requestApproval(id, deps.deploymentId, deps.now()); + return (await journal.get(id))!; + } + await current(call, signal); + const [connection] = await db + .select() + .from(integrationConnection) + .where(eq(integrationConnection.id, call.connectionId)); + if (!connection?.mcpCredential.status || connection.mcpCredential.transition) + throw Error("MCP credential authority unavailable"); + const status = fromJson(CredentialStatusSchema, connection.mcpCredential.status); + if (status.context?.authorizationGeneration !== BigInt(call.authGeneration)) + throw Error("MCP credential authority changed"); + if ((await journal.admit(id, deps.deploymentId, deps.now())) === "queued") + return (await journal.get(id))!; + const lease = (await journal.getAdmission(id))!; + const worker = client(resolved, signal); + try { + const identity = await worker.identity(); + await current(call, signal); + const operation: Extract = { + kind: "execute", + endpoint: resolved.binding.config.endpoint, + tool: resolved.reviewedTool.upstreamName, + arguments: argumentsValue(call.originalArguments!), + inputSchema: resolved.reviewedTool.inputSchema, + ...(resolved.reviewedTool.outputSchema + ? { outputSchema: resolved.reviewedTool.outputSchema } + : {}), + argumentConstraintDocuments: resolved.effectiveGrant.argumentConstraintDocuments, + classification: resolved.reviewedTool.classification, + }; + const dispatchIntent = intent(call, lease, identity, operation); + const proof: McpLiveDispatchAuthorization = { + executionId: id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: call.version, + reservationId: lease.reservationId, + reservationExpiresAt: lease.expiresAt, + }; + call = await journal.dispatch( + id, + { bootId: identity.bootId, intentId: dispatchIntent.id, expiresAt: lease.expiresAt }, + deps.now(), + proof, + ); + const dispatched = call; + const broker = makeCredentialBroker({ + control: deps.control, + now: () => deps.now().getTime(), + admission: { + async assertAdmitted() { + await current(dispatched, signal); + await journal.assertDispatchCurrent(id, proof, deps.now()); + }, + }, + worker: { + identity: () => worker.identity(), + async dispatch(request) { + enabled(resolved, signal); + await fence( + requestPolicyContext(dispatched.requestContext!), + resolved, + dispatched.originalArguments!, + ); + // Recheck synchronous kill/cancel inputs after the awaited coordinator response. + enabled(resolved, signal); + // No intervening await, identity refresh or socket replacement after this fence. + return worker.dispatch(request); + }, + }, + }); + let outcome: ReturnType; + try { + const response = await broker.dispatch({ + status, + identity, + intent: dispatchIntent, + operation, + }); + if (!response.ok) throw Error("MCP result unavailable"); + outcome = verifiedExecutionResult(response.result); + } catch { + outcome = { + state: call.classification === "write" ? "outcome_unknown" : "failed", + result: { + outcome: call.classification === "write" ? "outcome_unknown" : "failed", + delivery: "unavailable", + reason: "upstream_failure", + }, + resultUnavailable: false, + }; + } + return persistVerifiedOutcome(journal, dispatched, outcome, deps.now); + } finally { + worker.close(); + } + }, + }; +} diff --git a/orchestrator/src/mcp/journal.ts b/orchestrator/src/mcp/journal.ts index 925cd9916..c493885fb 100644 --- a/orchestrator/src/mcp/journal.ts +++ b/orchestrator/src/mcp/journal.ts @@ -648,6 +648,82 @@ export function makeMcpJournal(db = getDb()) { return call; }); }, + /** Trusted local validation failure, before any irreversible dispatch. */ + async rejectBeforeDispatch( + id: string, + expected: { + requestDigest: string; + version: number; + reservation: { reservationId: string; expiresAt: Date } | null; + }, + now: Date, + ) { + expected = structuredClone(expected); + const [observed] = await db.select().from(admission).where(eq(admission.executionId, id)); + return db.transaction(async (tx) => { + if (observed) await lockScope(tx, observed.deploymentId); + const [identity] = await tx.select().from(execution).where(eq(execution.id, id)); + if (!identity) throw new Error("call not found"); + await tx + .select() + .from(connection) + .where(eq(connection.id, identity.connectionId)) + .for("update"); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + const [lease] = await tx + .select() + .from(admission) + .where(eq(admission.executionId, id)) + .for("update"); + if ( + !call || + !["waiting_approval", "authorized"].includes(call.state) || + call.version !== expected.version || + call.requestDigest !== expected.requestDigest || + call.intentId || + (expected.reservation + ? !observed || + !lease || + lease.deploymentId !== observed.deploymentId || + lease.reservationId !== expected.reservation.reservationId || + lease.expiresAt.getTime() !== expected.reservation.expiresAt.getTime() + : Boolean(lease)) + ) + throw new Error("stale pre-dispatch rejection"); + const [updated] = await tx + .update(execution) + .set({ + state: "failed", + version: call.version + 1, + result: { + source: "local_validation", + delivery: "available", + output: { + content: [ + { type: "text", text: "Tool arguments do not satisfy the approved schemas." }, + ], + isError: true, + }, + }, + resultUnavailable: false, + updatedAt: now, + }) + .where(eq(execution.id, id)) + .returning(); + await tx.delete(admission).where(eq(admission.executionId, id)); + await tx + .update(approval) + .set({ decision: "denied", decidedAt: now }) + .where( + and( + eq(approval.executionId, id), + isNull(approval.consumedAt), + inArray(approval.decision, ["pending", "approved"]), + ), + ); + return updated!; + }); + }, async finish( id: string, version: number, From e669c7d381e50fc7f8a94c512d3bed93aefe9d9d Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 04:53:22 -0500 Subject: [PATCH 025/279] Check MCP credential expiry after the final authority fence --- .../__tests__/mcp-credential-broker.test.ts | 19 ++++++++++++- .../src/__tests__/mcp-effect-driver.test.ts | 28 +++++++++++++++++++ .../src/__tests__/mcp-effect-fixture.ts | 6 +++- orchestrator/src/mcp/credential-broker.ts | 4 +++ orchestrator/src/mcp/effect-driver.ts | 24 +++++++--------- 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/orchestrator/src/__tests__/mcp-credential-broker.test.ts b/orchestrator/src/__tests__/mcp-credential-broker.test.ts index 02ddee08e..958be0971 100644 --- a/orchestrator/src/__tests__/mcp-credential-broker.test.ts +++ b/orchestrator/src/__tests__/mcp-credential-broker.test.ts @@ -36,7 +36,7 @@ const status = create(CredentialStatusSchema, { status: "connected", version: 1n, }); -function fixture(expire = false) { +function fixture(expire = false, finalFenceMs = 0) { let time = now, deliveries = 0, leases = 0; @@ -66,6 +66,9 @@ function fixture(expire = false) { const broker = makeCredentialBroker({ control, now: () => time, + async beforeDispatch() { + time += finalFenceMs; + }, admission: { async assertAdmitted() { return; @@ -288,3 +291,17 @@ test("worker transport errors cannot expose the transient credential", async () }); await expect(broker.dispatch(f.input)).rejects.toThrow("MCP worker operation unavailable"); }); + +for (const finalFenceMs of [9999, 10000, 11000]) + test(`final fence at ${finalFenceMs} ms preserves the delivery boundary and full operation deadline`, async () => { + const f = fixture(false, finalFenceMs); + if (finalFenceMs < 10000) { + await f.broker.dispatch(f.input); + expect(f.counters()).toEqual({ deliveries: 1, leases: 1 }); + } else { + await expect(f.broker.dispatch(f.input)).rejects.toThrow("credential delivery expired"); + expect(f.counters()).toEqual({ deliveries: 0, leases: 1 }); + } + expect(f.seen[0]).toMatchObject({ expiresAt: 110000n, operationExpiresAt: 160000n }); + expect(f.input.intent.expiresAt).toBe(160000); + }); diff --git a/orchestrator/src/__tests__/mcp-effect-driver.test.ts b/orchestrator/src/__tests__/mcp-effect-driver.test.ts index 1dc85fedf..ad6f4cb8e 100644 --- a/orchestrator/src/__tests__/mcp-effect-driver.test.ts +++ b/orchestrator/src/__tests__/mcp-effect-driver.test.ts @@ -583,3 +583,31 @@ test("credential assertion rejects an altered execution reservation after leasin await f.cleanup(); } }, 20000); + +test("credential delivery expiry during only the final fence prevents send and replay", async () => { + const f = await effectFixture({ classification: "write" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(call.id); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); + let postLeaseFences = 0; + f.beforeFence(async () => { + if (f.leases && ++postLeaseFences === 2) f.advanceNow(11000); + }); + const result = await f.driver.driveOnce(call.id); + expect(postLeaseFences).toBe(2); + // Only the ten-second credential handoff expires; the full tool deadline stays live. + expect(result.intentExpiresAt!.getTime() - (Date.now() + 11000)).toBeGreaterThan(45000); + expect(result.state).toBe("outcome_unknown"); + expect(f.leases).toBe(1); + expect((await f.command("stats")).credentialRequests).toBe(0); + expect((await f.command("stats")).calls).toBe(0); + expect((await f.driver.acceptToolRequest(f.request)).id).toBe(call.id); + expect((await f.driver.driveOnce(call.id)).state).toBe("outcome_unknown"); + expect(f.leases).toBe(1); + expect((await f.command("stats")).credentialRequests).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-effect-fixture.ts b/orchestrator/src/__tests__/mcp-effect-fixture.ts index 80fe2f08c..f935d453f 100644 --- a/orchestrator/src/__tests__/mcp-effect-fixture.ts +++ b/orchestrator/src/__tests__/mcp-effect-fixture.ts @@ -140,6 +140,7 @@ export async function effectFixture( token = "known-literal-token", leases = 0; let enabled = true; + let clockOffsetMs = 0; let beforeFence: (() => Promise) | undefined; let beforeLease: (() => Promise) | undefined; let afterLease: (() => Promise) | undefined; @@ -272,7 +273,7 @@ export async function effectFixture( serverFingerprint: tls.fingerprint, }), enabled: () => enabled, - now: () => new Date(), + now: () => new Date(Date.now() + clockOffsetMs), }); return { ...f, @@ -285,6 +286,9 @@ export async function effectFixture( events, policies, operatorPolicies, + advanceNow(milliseconds: number) { + clockOffsetMs += milliseconds; + }, setEnabled(value: boolean) { enabled = value; }, diff --git a/orchestrator/src/mcp/credential-broker.ts b/orchestrator/src/mcp/credential-broker.ts index f610cc311..f5e660905 100644 --- a/orchestrator/src/mcp/credential-broker.ts +++ b/orchestrator/src/mcp/credential-broker.ts @@ -12,6 +12,7 @@ import type { CredentialControlClient } from "./control-client.ts"; export interface McpResourceWorker { identity(): Promise; + /** Fixed send only. Put awaited policy checks in the broker beforeDispatch hook. */ dispatch(request: WorkerRequest): Promise; } type ResourceOperation = Extract; @@ -30,6 +31,8 @@ export function makeCredentialBroker(deps: { worker: McpResourceWorker; admission: McpResourceAdmission; now: () => number; + /** Final authority fence; receives no credentials and runs before the last delivery-expiry check. */ + beforeDispatch?(): Promise; }) { if (!deps.admission?.assertAdmitted) throw new Error("MCP global admission is required"); return { @@ -114,6 +117,7 @@ export function makeCredentialBroker(deps: { }); if (deps.now() >= deliveryExpiry) throw new Error("credential delivery expired"); await deps.admission.assertAdmitted(structuredClone(frozen)); + if (deps.beforeDispatch) await deps.beforeDispatch(); if (deps.now() >= deliveryExpiry) throw new Error("credential delivery expired"); let response: WorkerResponse; try { diff --git a/orchestrator/src/mcp/effect-driver.ts b/orchestrator/src/mcp/effect-driver.ts index acea216f8..caa61d49d 100644 --- a/orchestrator/src/mcp/effect-driver.ts +++ b/orchestrator/src/mcp/effect-driver.ts @@ -467,20 +467,16 @@ export function makeMcpEffectDriver(deps: { await journal.assertDispatchCurrent(id, proof, deps.now()); }, }, - worker: { - identity: () => worker.identity(), - async dispatch(request) { - enabled(resolved, signal); - await fence( - requestPolicyContext(dispatched.requestContext!), - resolved, - dispatched.originalArguments!, - ); - // Recheck synchronous kill/cancel inputs after the awaited coordinator response. - enabled(resolved, signal); - // No intervening await, identity refresh or socket replacement after this fence. - return worker.dispatch(request); - }, + worker, + async beforeDispatch() { + enabled(resolved, signal); + await fence( + requestPolicyContext(dispatched.requestContext!), + resolved, + dispatched.originalArguments!, + ); + // The broker checks delivery expiry next, then sends on the retained socket. + enabled(resolved, signal); }, }); let outcome: ReturnType; From fe143a9c2e0e381022bc7fb11e49e78dd8b9378c Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 05:42:09 -0500 Subject: [PATCH 026/279] Recover MCP calls through durable reference workflows --- mcp-worker/test/fixtures/effect-worker.ts | 23 +- .../drizzle/0111_mcp_completion_receipt.sql | 28 + orchestrator/drizzle/meta/0111_snapshot.json | 8541 +++++++++++++++++ orchestrator/drizzle/meta/_journal.json | 7 + .../src/__tests__/helpers/mcp-dbos-child.ts | 275 + .../__tests__/helpers/mcp-test-isolation.ts | 30 + .../src/__tests__/mcp-dbos-restart.test.ts | 654 ++ .../src/__tests__/mcp-effect-fixture.ts | 30 +- .../src/__tests__/mcp-recovery.test.ts | 496 + orchestrator/src/db/schema.ts | 146 +- orchestrator/src/index.ts | 31 +- .../listeners/__tests__/tool-consumer.test.ts | 90 +- orchestrator/src/listeners/tool-consumer.ts | 77 +- orchestrator/src/mcp/effect-driver.ts | 72 +- orchestrator/src/mcp/recovery-scanner.ts | 58 + orchestrator/src/mcp/recovery.ts | 275 + orchestrator/src/mcp/session-resolver.ts | 35 +- .../src/sweep/__tests__/sweeper.test.ts | 113 +- orchestrator/src/sweep/policy.ts | 17 +- orchestrator/src/sweep/sweeper.ts | 117 +- orchestrator/src/workflows/mcp-exec.ts | 73 + 21 files changed, 10874 insertions(+), 314 deletions(-) create mode 100644 orchestrator/drizzle/0111_mcp_completion_receipt.sql create mode 100644 orchestrator/drizzle/meta/0111_snapshot.json create mode 100644 orchestrator/src/__tests__/helpers/mcp-dbos-child.ts create mode 100644 orchestrator/src/__tests__/helpers/mcp-test-isolation.ts create mode 100644 orchestrator/src/__tests__/mcp-dbos-restart.test.ts create mode 100644 orchestrator/src/__tests__/mcp-recovery.test.ts create mode 100644 orchestrator/src/mcp/recovery-scanner.ts create mode 100644 orchestrator/src/mcp/recovery.ts create mode 100644 orchestrator/src/workflows/mcp-exec.ts diff --git a/mcp-worker/test/fixtures/effect-worker.ts b/mcp-worker/test/fixtures/effect-worker.ts index 2d5bab476..5e6e088c5 100644 --- a/mcp-worker/test/fixtures/effect-worker.ts +++ b/mcp-worker/test/fixtures/effect-worker.ts @@ -6,6 +6,7 @@ import { ZonePolicy } from "../../src/network.ts"; const lines = createInterface({ input: process.stdin }); const input = lines[Symbol.asyncIterator](); const { tls, response = "normal" } = JSON.parse((await input.next()).value!); +const held: (() => void)[] = []; let calls = 0, credentialRequests = 0; const provider = createServer(tls, (req, res) => { @@ -24,7 +25,22 @@ const provider = createServer(tls, (req, res) => { }; else if (message.method === "tools/call") { calls++; - if (response === "hold") return; + if (response === "hold") { + held.push(() => { + res.writeHead(200, { "content-type": "application/json" }); + res.end( + JSON.stringify({ + jsonrpc: "2.0", + id: message.id, + result: { + resultType: "complete", + content: [{ type: "text", text: "completed" }], + }, + }), + ); + }); + return; + } if (response === "malformed") { res.writeHead(200, { "content-type": "application/json" }); res.end("{broken"); @@ -96,7 +112,10 @@ const send = (value: unknown) => send({ port, identity: worker.identity, node: process.versions.node }); for await (const command of { [Symbol.asyncIterator]: () => input }) { if (command === "stats") send({ calls, credentialRequests }); - else if (command === "restart") { + else if (command === "release") { + for (const release of held.splice(0)) release(); + send({ released: true }); + } else if (command === "restart") { await worker.close(); worker = make(); await worker.listen(port, "127.0.0.1"); diff --git a/orchestrator/drizzle/0111_mcp_completion_receipt.sql b/orchestrator/drizzle/0111_mcp_completion_receipt.sql new file mode 100644 index 000000000..7527b3175 --- /dev/null +++ b/orchestrator/drizzle/0111_mcp_completion_receipt.sql @@ -0,0 +1,28 @@ +ALTER TABLE "mcp_execution" ADD COLUMN "completion_enqueued_version" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +CREATE INDEX "mcp_execution_recovery_idx" ON "mcp_execution" USING btree ("id") WHERE "mcp_execution"."completion_enqueued_version" < "mcp_execution"."version";--> statement-breakpoint +ALTER TABLE "mcp_execution" ADD CONSTRAINT "mcp_execution_completion_version_check" CHECK ("mcp_execution"."completion_enqueued_version" >= 0 AND "mcp_execution"."completion_enqueued_version" <= "mcp_execution"."version"); +--> statement-breakpoint +CREATE OR REPLACE FUNCTION mcp_execution_transition() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF (to_jsonb(NEW) - 'completion_enqueued_version') = (to_jsonb(OLD) - 'completion_enqueued_version') + AND OLD.state IN ('succeeded','failed','canceled','outcome_unknown') + AND NEW.completion_enqueued_version >= OLD.completion_enqueued_version + AND NEW.completion_enqueued_version <= OLD.version THEN + RETURN NEW; + END IF; + IF (to_jsonb(NEW) - ARRAY['state','version','boot_id','intent_id','intent_expires_at','result','result_unavailable','updated_at']) IS DISTINCT FROM + (to_jsonb(OLD) - ARRAY['state','version','boot_id','intent_id','intent_expires_at','result','result_unavailable','updated_at']) THEN + RAISE EXCEPTION 'MCP request identity is immutable'; + END IF; + IF NOT ((OLD.state = 'waiting_approval' AND NEW.state IN ('authorized','canceled','failed')) OR + (OLD.state = 'authorized' AND NEW.state IN ('dispatching','canceled','failed')) OR + (OLD.state = 'dispatching' AND NEW.state IN ('succeeded','failed','outcome_unknown')) OR + (OLD.state = 'outcome_unknown' AND NEW.state IN ('succeeded','failed'))) OR NEW.version <> OLD.version + 1 THEN + RAISE EXCEPTION 'Invalid MCP execution transition'; + END IF; + IF OLD.intent_id IS NOT NULL AND (NEW.intent_id, NEW.boot_id, NEW.intent_expires_at) IS DISTINCT FROM (OLD.intent_id, OLD.boot_id, OLD.intent_expires_at) THEN + RAISE EXCEPTION 'MCP dispatch intent is immutable'; + END IF; + RETURN NEW; +END; +$$; diff --git a/orchestrator/drizzle/meta/0111_snapshot.json b/orchestrator/drizzle/meta/0111_snapshot.json new file mode 100644 index 000000000..32781f759 --- /dev/null +++ b/orchestrator/drizzle/meta/0111_snapshot.json @@ -0,0 +1,8541 @@ +{ + "id": "fc07ee98-e376-4cb1-8a53-6be93ee7a8f8", + "prevId": "21a2d61b-0099-4962-8ffc-85a12a935482", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_credential": { + "name": "mcp_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()::text" + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'execute'" + }, + "schema_queue_expires_at": { + "name": "schema_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_queue_expires_at": { + "name": "execute_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_expires_at": { + "name": "execute_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "schema_digest": { + "name": "schema_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_reservation_idx": { + "name": "mcp_admission_reservation_idx", + "columns": [ + { + "expression": "reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_phase_check": { + "name": "mcp_admission_phase_check", + "value": "\"mcp_admission\".\"phase\" IN ('schema','execute')" + }, + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted','released')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_arguments": { + "name": "original_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completion_enqueued_version": { + "name": "completion_enqueued_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_recovery_idx": { + "name": "mcp_execution_recovery_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_execution\".\"completion_enqueued_version\" < \"mcp_execution\".\"version\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_completion_version_check": { + "name": "mcp_execution_completion_version_check", + "value": "\"mcp_execution\".\"completion_enqueued_version\" >= 0 AND \"mcp_execution\".\"completion_enqueued_version\" <= \"mcp_execution\".\"version\"" + }, + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "mcp_grant_publication": { + "name": "mcp_grant_publication", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_binding_envelope": { + "name": "mcp_binding_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/_journal.json b/orchestrator/drizzle/meta/_journal.json index 8b3a32407..fac76d50e 100644 --- a/orchestrator/drizzle/meta/_journal.json +++ b/orchestrator/drizzle/meta/_journal.json @@ -778,6 +778,13 @@ "when": 1793611948671, "tag": "0110_mcp_preflight_rejection", "breakpoints": true + }, + { + "idx": 111, + "version": "7", + "when": 1793613661405, + "tag": "0111_mcp_completion_receipt", + "breakpoints": true } ] } \ No newline at end of file diff --git a/orchestrator/src/__tests__/helpers/mcp-dbos-child.ts b/orchestrator/src/__tests__/helpers/mcp-dbos-child.ts new file mode 100644 index 000000000..05add1b4c --- /dev/null +++ b/orchestrator/src/__tests__/helpers/mcp-dbos-child.ts @@ -0,0 +1,275 @@ +import { assertSweepPoliciesExhaustive } from "../../sweep/policy.ts"; +import { DBOS } from "@dbos-inc/dbos-sdk"; +import { createInterface } from "node:readline"; +import { Client } from "pg"; +import { fromJson } from "@bufbuild/protobuf"; +import { createClient, createRouterTransport } from "@connectrpc/connect"; +import { and, eq } from "drizzle-orm"; +import { getDb } from "../../db/client.ts"; +import { mcpExecution } from "../../db/schema.ts"; +import { CredentialControlService } from "../../gen/engram/mcp/v1/control_pb.ts"; +import { SessionPolicyService } from "../../gen/engram/mcp/v1/session_policy_pb.ts"; +import { McpWritePolicyRefSchema } from "../../gen/engram/app/v1/session_pb.ts"; +import { makeStoredSessionBindingResolver } from "../../mcp/binding-store.ts"; +import { unwrapCredentialControlClient } from "../../mcp/control-client.ts"; +import { makeMcpEffectDriver } from "../../mcp/effect-driver.ts"; +import { makeMcpJournal } from "../../mcp/journal.ts"; +import { makeMcpRecovery, installMcpRecovery } from "../../mcp/recovery.ts"; +import { makeMcpRecoveryScanner } from "../../mcp/recovery-scanner.ts"; +import { startMcpRequestWorkflow } from "../../workflows/mcp-exec.ts"; +import { makeToolConsumer } from "../../listeners/tool-consumer.ts"; +import { createToolRegistry } from "../../tools/registry.ts"; +import { makePendingToolCallStore } from "../../tools/pending-tool-calls.ts"; +import { verifyMcpTestDatabases } from "./mcp-test-isolation.ts"; +import type { effectFixture } from "../mcp-effect-fixture.ts"; + +type Fixture = Awaited>; +interface Input { + remote: Fixture["remote"]; + request: Fixture["request"]; + deploymentId: string; + checkpoint?: string; + workflowId: string; + resume?: boolean; + clockOffset?: number; + eventIdx?: string; + requestReadFailures?: number; +} +const lines = createInterface({ input: process.stdin }); +const input: Input = JSON.parse((await lines[Symbol.asyncIterator]().next()).value!); +const { system } = await verifyMcpTestDatabases(); +const now = () => new Date(Date.now() + (input.clockOffset ?? 0)); +let releaseCheckpoint: (() => void) | undefined; +let postLeaseFences = 0; +let responseRead = false; +async function checkpoint(name: string) { + if (name !== input.checkpoint) return; + process.stdout.write(`CHECKPOINT ${name}\n`); + await new Promise((resolve) => { + releaseCheckpoint = resolve; + }); // Parent kills or releases the observed process. +} +// Test-only driver instrumentation observes real SQL before/after execution and commit. +// It never changes SQL, transaction results, authority, or the production effect path. +const transitions = new WeakMap(); +Client.prototype.query = new Proxy(Client.prototype.query, { + apply(target, client, args) { + const query = typeof args[0] === "string" ? args[0] : args[0]?.text; + const values = args[1] ?? args[0]?.values ?? []; + return (async () => { + if (responseRead && query?.startsWith("select") && query.includes('from "mcp_execution"')) { + responseRead = false; + await checkpoint("response_before_persist"); + } + if (query?.startsWith('update "mcp_admission"') && values.includes("execute")) + await checkpoint("before_admission"); + if (query?.startsWith('update "mcp_approval" set "consumed_at"')) + await checkpoint("approval_before_consume"); + if (query?.startsWith('insert into "mcp_execution"')) { + await checkpoint("before_insert"); + transitions.set(client, "after_insert"); + } + if (query?.startsWith('update "mcp_execution" set "state"')) { + if (values[0] === "dispatching") { + await checkpoint("intent_issued"); + transitions.set(client, "barrier_committed"); + } + if (["succeeded", "failed"].includes(values[0])) { + await checkpoint("response_verified"); + transitions.set(client, "result_committed"); + } + } + const result = await Reflect.apply(target, client, args); + if (query?.startsWith('update "mcp_approval" set "consumed_at"')) + await checkpoint("approval_consumed_uncommitted"); + if (query?.toLowerCase() === "commit") { + const boundary = transitions.get(client); + transitions.delete(client); + if (boundary) await checkpoint(boundary); + } + if (query?.startsWith('update "mcp_execution" set "completion_enqueued_version"')) + await checkpoint("receipt_committed"); + return result; + })(); + }, +}); +const db = getDb(), + journal = makeMcpJournal(db); +const ref = fromJson(McpWritePolicyRefSchema, input.remote.policyRef); +let leases = 0; +async function fence(req: { + toolCallId: string; + originalArguments: string; + modelToolName: string; +}) { + const [call] = await db + .select() + .from(mcpExecution) + .where( + and( + eq(mcpExecution.sessionId, input.request.sessionId), + eq(mcpExecution.toolCallId, input.request.toolCallId), + ), + ); + if (call) { + const lease = await journal.getAdmission(call.id); + if (call.state === "authorized" && lease?.phase === "execute" && lease.state === "admitted") + await checkpoint("admission_committed"); + if (call.state === "dispatching" && leases > 0) await checkpoint("intent_leased"); + } + if (leases > 0 && ++postLeaseFences === 2) responseRead = true; + return { + protocol: 1, + status: + req.toolCallId === input.request.toolCallId && + req.originalArguments === input.request.originalArguments && + req.modelToolName === input.request.modelToolName + ? "current" + : "stale", + }; +} +const policy = createClient( + SessionPolicyService, + createRouterTransport((router) => + router.service(SessionPolicyService, { + readWritePolicy: async () => ({ + state: { + policy: { + current: true, + initialized: input.remote.eligibility !== "unknown", + writeEligibility: input.remote.eligibility, + ref, + ancestorPolicyRefs: [], + }, + }, + }), + validateExecutionIdentity: fence, + validateDispatchFence: async (req) => + input.remote.eligibility === "write_eligible" + ? fence(req) + : { protocol: 1, status: "stale" }, + }), + ), +); +const control = unwrapCredentialControlClient( + createClient( + CredentialControlService, + createRouterTransport((router) => + router.service(CredentialControlService, { + lease: async (req) => { + leases++; + await checkpoint("before_lease"); + return { + protocol: 1, + intentId: req.intentId, + expiresAt: req.expiresAt, + operationExpiresAt: req.operationExpiresAt, + header: "authorization", + value: "Bearer known-literal-token", + }; + }, + }), + ), + ), +); +const resolver = makeStoredSessionBindingResolver( + { + control: policy, + readOperatorPolicies: async () => input.remote.operatorPolicies, + enforceManagedDeny: async (expected) => expected, + }, + db, +); +const driver = makeMcpEffectDriver({ + db, + deploymentId: input.deploymentId, + control, + policy, + resolver, + workerTls: () => input.remote.workerTls, + enabled: () => true, + now, +}); +// The coordinator completion boundary is a parent-owned deterministic fixture. It stores a +// fixed-key outbox record before returning, and survives driver process death. +const completions: (() => void)[] = []; +lines.on("line", (line) => { + if (line === "ACK") completions.shift()?.(); + if (line === "CONTINUE") releaseCheckpoint?.(); +}); +const runtime = makeMcpRecovery({ + db, + driver, + deploymentId: input.deploymentId, + now, + readRequestEvent: async (sessionId, index) => { + if (input.requestReadFailures && input.requestReadFailures-- > 0) { + await checkpoint("request_read_failed"); + throw Error("private-control-error"); + } + if (sessionId !== input.request.sessionId || index !== BigInt(input.eventIdx ?? "1")) + return undefined; + return { + idx: index, + kind: "tool_call_requested", + payloadJson: JSON.stringify({ + run_id: input.request.runId, + tool_call_id: input.request.toolCallId, + name: input.request.modelToolName, + args_json: input.request.originalArguments, + }), + }; + }, + completer: { + async completeToolCall(request) { + await checkpoint("before_enqueue"); + const ack = new Promise((resolve) => completions.push(resolve)); + process.stdout.write(`COMPLETE ${JSON.stringify(request)}\n`); + await ack; + await checkpoint("after_enqueue_ack"); + }, + }, +}); +installMcpRecovery(runtime); +assertSweepPoliciesExhaustive(); +DBOS.setConfig({ + name: "mcp-restart-test", + systemDatabaseUrl: system, + executorID: input.workflowId, + applicationVersion: "mcp-restart-v1", + runAdminServer: false, +}); +await DBOS.launch(); +const scanner = makeMcpRecoveryScanner({ + deploymentId: input.deploymentId, + journal, + runtime: () => runtime, + now, +}); +if (!input.resume) { + const consumer = makeToolConsumer({ + registry: createToolRegistry(), + pendingCalls: makePendingToolCallStore(db), + now, + startWorkflow: async () => { + throw Error("static workflow used"); + }, + startMcpRequest: startMcpRequestWorkflow, + }); + await consumer.handle( + { + idx: BigInt(input.eventIdx ?? "1"), + kind: "tool_call_requested", + payloadJson: JSON.stringify({ + run_id: input.request.runId, + tool_call_id: input.request.toolCallId, + name: input.request.modelToolName, + args_json: input.request.originalArguments, + }), + }, + { sessionId: input.request.sessionId }, + ); +} +await scanner.start(); +process.stdout.write("READY\n"); +setInterval(() => {}, 1000); diff --git a/orchestrator/src/__tests__/helpers/mcp-test-isolation.ts b/orchestrator/src/__tests__/helpers/mcp-test-isolation.ts new file mode 100644 index 000000000..0055a324d --- /dev/null +++ b/orchestrator/src/__tests__/helpers/mcp-test-isolation.ts @@ -0,0 +1,30 @@ +import { Pool } from "pg"; + +/** Verify both actual database identities before any DBOS configuration or fixture write. */ +export async function verifyMcpTestDatabases() { + const app = process.env.ORCHESTRATOR_DATABASE_URL; + const system = process.env.MCP_TEST_DBOS_DATABASE_URL; + const expected = process.env.MCP_TEST_DATABASE_NAME; + if (!app || !system || !expected) throw Error("explicit isolated MCP test databases required"); + const a = new URL(app), + b = new URL(system); + if ( + !/^engram_mcp_\d+_\d+_\d+$/.test(expected) || + a.pathname !== `/${expected}` || + !/^\/engram_mcp_dbos_test_[a-z0-9_]+$/.test(b.pathname) || + a.pathname === b.pathname || + [a, b].some((u) => u.hostname !== "127.0.0.1" || u.port !== "5435") + ) + throw Error("MCP test database isolation mismatch"); + for (const url of [a, b]) { + const pool = new Pool({ connectionString: url.href, connectionTimeoutMillis: 2000 }); + try { + const result = await pool.query("select current_database() as name"); + if (result.rows[0]?.name !== url.pathname.slice(1)) + throw Error("MCP test database identity mismatch"); + } finally { + await pool.end(); + } + } + return { app, system }; +} diff --git a/orchestrator/src/__tests__/mcp-dbos-restart.test.ts b/orchestrator/src/__tests__/mcp-dbos-restart.test.ts new file mode 100644 index 000000000..8347248db --- /dev/null +++ b/orchestrator/src/__tests__/mcp-dbos-restart.test.ts @@ -0,0 +1,654 @@ +import { beforeAll, afterAll, expect, test } from "bun:test"; +import { spawn } from "node:child_process"; +import { createInterface } from "node:readline"; +import { once } from "node:events"; +import { Pool } from "pg"; +import { and, eq } from "drizzle-orm"; +import { effectFixture } from "./mcp-effect-fixture.ts"; +import { mcpExecution, mcpApproval } from "../db/schema.ts"; +import { reviewContext } from "../mcp/journal-review.ts"; +import { verifyMcpTestDatabases } from "./helpers/mcp-test-isolation.ts"; + +const isolated = await verifyMcpTestDatabases(); +const system = new Pool({ connectionString: isolated.system }); +beforeAll(async () => { + await system.query("create schema if not exists mcp_test"); + await system.query( + `create table if not exists mcp_test.completions (session_id text, call_id text, result_json text not null, attempts integer not null default 1, primary key(session_id,call_id))`, + ); +}); +afterAll(() => system.end()); +async function until(description: string, check: () => Promise, timeout = 20000) { + const end = Date.now() + timeout; + while (Date.now() < end) { + if (await check()) return; + await Bun.sleep(20); + } + throw Error(`timeout: ${description}`); +} +type Fixture = Awaited>; +async function start( + f: Fixture, + executor: string, + checkpoint?: string, + resume = false, + clockOffset = 0, + eventIdx = "1", + requestReadFailures = 0, +) { + const child = spawn( + process.execPath, + [new URL("./helpers/mcp-dbos-child.ts", import.meta.url).pathname], + { + env: { + PATH: process.env.PATH, + ORCHESTRATOR_DATABASE_URL: isolated.app, + MCP_TEST_DBOS_DATABASE_URL: isolated.system, + MCP_TEST_DATABASE_NAME: process.env.MCP_TEST_DATABASE_NAME, + NODE_ENV: "test", + LOG_LEVEL: "silent", + }, + stdio: ["pipe", "pipe", "pipe"], + }, + ); + let reached = false, + ready = false, + stderr = "", + failure: unknown; + let completions = Promise.resolve(); + const lines = createInterface({ input: child.stdout }); + lines.on("line", (line) => { + if (line === `CHECKPOINT ${checkpoint}`) reached = true; + if (line === "READY") ready = true; + if (line.startsWith("COMPLETE ")) { + const data: { sessionId: string; toolCallId: string; resultJson: string } = JSON.parse( + line.slice(9), + ); + completions = completions + .then(async () => { + // Explicit coordinator fixture: fixed-key durable enqueue precedes ACK. No effect proof. + await system.query( + `insert into mcp_test.completions(session_id,call_id,result_json) values($1,$2,$3) + on conflict(session_id,call_id) do update set attempts=mcp_test.completions.attempts+1`, + [data.sessionId, data.toolCallId, data.resultJson], + ); + if (checkpoint === "enqueue_before_ack") { + reached = true; + return; + } + child.stdin.write("ACK\n"); + }) + .catch((error) => { + failure = error; + }); + } + }); + child.stderr.on("data", (value) => { + stderr += String(value); + }); + child.stdin.write( + JSON.stringify({ + remote: f.remote, + request: f.request, + deploymentId: f.deploymentId, + workflowId: executor, + checkpoint, + resume, + clockOffset, + eventIdx, + requestReadFailures, + }) + "\n", + ); + return { + async reached() { + await until(`child ${checkpoint}`, async () => { + if (failure) throw failure; + if (child.exitCode !== null) + throw Error( + `driver exited ${child.exitCode}: ${stderr.replace(/postgres(?:ql)?:\/\/[^\s]+/g, "[database]")}`, + ); + return reached; + }); + }, + async ready() { + await until("driver ready", async () => { + if (child.exitCode !== null) throw Error(`driver exited: ${stderr}`); + return ready; + }); + }, + continue() { + child.stdin.write("CONTINUE\n"); + }, + async stop() { + if (child.exitCode === null && child.signalCode === null) { + const done = once(child, "exit"); + child.kill("SIGKILL"); + await done; + } + await completions; + lines.close(); + }, + }; +} +async function callFor(f: Fixture) { + return ( + await f.db + .select() + .from(mcpExecution) + .where( + and( + eq(mcpExecution.sessionId, f.sessionId), + eq(mcpExecution.toolCallId, f.request.toolCallId), + ), + ) + )[0]; +} +async function approve(f: Fixture) { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(call.id); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); +} +const boundaries = [ + "before_insert", + "after_insert", + "before_admission", + "admission_committed", + "approval_before_consume", + "approval_consumed_uncommitted", + "intent_issued", + "barrier_committed", + "before_lease", + "intent_leased", + "response_verified", + "result_committed", + "before_enqueue", + "enqueue_before_ack", + "after_enqueue_ack", + "receipt_committed", +]; +for (const boundary of boundaries) + test(`actual DBOS driver process restart at ${boundary}`, async () => { + const insert = boundary === "before_insert" || boundary === "after_insert"; + const f = await effectFixture({ classification: "write" }); + const executor = `mcp-test-${crypto.randomUUID()}`; + let child: Awaited> | undefined; + try { + if (!insert) await approve(f); + child = await start(f, executor, boundary); + await child.reached(); + const before = await callFor(f); + const beforeCalls = (await f.command("stats")).calls; + const beforeLease = before ? await f.journal.getAdmission(before.id) : null; + await child.stop(); + const afterBarrier = [ + "barrier_committed", + "before_lease", + "intent_leased", + "response_verified", + ].includes(boundary); + child = await start( + f, + executor, + undefined, + boundary !== "before_insert", + afterBarrier ? 61000 : 0, + ); + await child.ready(); + if (insert) { + await until("recovered schema and approval", async () => { + const call = await callFor(f); + return ( + !!call && (await f.journal.getAdmission(call.id))?.schemaDigest === call.requestDigest + ); + }); + const call = (await callFor(f))!; + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); + } + await until("committed completion receipt", async () => { + const call = await callFor(f); + return !!call && call.completionEnqueuedVersion === call.version; + }); + const final = (await callFor(f))!; + expect(final.originalArguments).toBe(f.request.originalArguments); + if (before) { + expect(final.requestDigest).toBe(before.requestDigest); + expect(final.deadline).toEqual(before.deadline); + } + if (beforeLease?.phase === "execute") + expect(final.intentExpiresAt).toEqual(beforeLease.expiresAt); + expect(final.state).toBe(afterBarrier ? "outcome_unknown" : "succeeded"); + const finalCalls = (await f.command("stats")).calls; + expect(finalCalls).toBe(afterBarrier ? beforeCalls : 1); + expect(finalCalls).toBeLessThanOrEqual(1); + if (!insert) { + const approval = ( + await f.db.select().from(mcpApproval).where(eq(mcpApproval.executionId, final.id)) + )[0]!; + expect(approval.consumedAt).not.toBeNull(); + } + const completion = ( + await system.query( + "select * from mcp_test.completions where session_id=$1 and call_id=$2", + [f.sessionId, f.request.toolCallId], + ) + ).rows[0]; + expect(completion).toBeDefined(); + const delivered = JSON.parse(completion.result_json); + if (afterBarrier) + expect(delivered).toMatchObject({ + source: "runtime", + outcome: "outcome_unknown", + delivery: "unavailable", + }); + else + expect(delivered).toMatchObject({ + outcome: "succeeded", + delivery: "available", + output: { isError: false }, + }); + if (["enqueue_before_ack", "after_enqueue_ack"].includes(boundary)) + expect(completion.attempts).toBeGreaterThanOrEqual(2); + // Inspect the actual DBOS database, not a serialization mock. + const status = await system.query( + "select inputs,output,error from dbos.workflow_status where executor_id=$1", + [executor], + ); + expect(status.rowCount).toBeGreaterThan(0); + for (const row of status.rows) { + const input = JSON.parse(row.inputs); + expect(input.json).toHaveLength(1); + expect(Object.keys(input.json[0]).sort()).toEqual( + input.json[0].requestEventIdx + ? ["deadlineMs", "requestEventIdx", "runId", "sessionId", "toolCallId"] + : ["executionId", "runId", "sessionId", "toolCallId"], + ); + if (row.output) expect(JSON.parse(row.output).json).toBeNull(); + expect(row.error).toBeNull(); + } + const durable = JSON.stringify(status.rows); + expect(durable).not.toContain("known-literal-token"); + expect(durable).not.toContain("originalArguments"); + expect(durable).not.toContain("structuredContent"); + expect(durable).not.toContain("resultJson"); + const steps = await system.query( + "select o.* from dbos.operation_outputs o join dbos.workflow_status w using(workflow_uuid) where w.executor_id=$1", + [executor], + ); + expect( + steps.rows.every((row) => + ["DBOS.sleep", "mcp-request-decision", "McpExecWorkflow"].includes(row.function_name), + ), + ).toBe(true); + expect(JSON.stringify(steps.rows)).not.toContain("known-literal-token"); + expect(JSON.stringify(steps.rows)).not.toContain("structuredContent"); + } finally { + await child?.stop(); + await system.query("delete from mcp_test.completions where session_id=$1", [f.sessionId]); + await f.cleanup(); + } + }, 45000); + +for (const eligibility of ["plan", "unknown"]) + test(`actual consumer and DBOS allow reads with ${eligibility} write policy`, async () => { + const f = await effectFixture({ eligibility }); + let child: Awaited> | undefined; + try { + child = await start(f, `mcp-test-${crypto.randomUUID()}`); + await child.ready(); + await until("read completion", async () => { + const row = await callFor(f); + return row?.state === "succeeded" && row.completionEnqueuedVersion === row.version; + }); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await child?.stop(); + await system.query("delete from mcp_test.completions where session_id=$1", [f.sessionId]); + await f.cleanup(); + } + }, 30000); + +for (const eligibility of ["plan", "unknown"]) + test(`actual consumer completes a ${eligibility} write denial and permits the next read`, async () => { + const f = await effectFixture({ eligibility, classification: "write" }); + let child: Awaited> | undefined; + try { + child = await start(f, `mcp-test-${crypto.randomUUID()}`); + await child.ready(); + await until( + "negative completion", + async () => + !!( + await system.query( + "select 1 from mcp_test.completions where session_id=$1 and call_id=$2", + [f.sessionId, f.request.toolCallId], + ) + ).rowCount, + ); + expect(await callFor(f)).toBeUndefined(); + expect((await f.command("stats")).calls).toBe(0); + const denied = ( + await system.query( + "select result_json from mcp_test.completions where session_id=$1 and call_id=$2", + [f.sessionId, f.request.toolCallId], + ) + ).rows[0]; + expect(JSON.parse(denied.result_json)).toMatchObject({ + source: "runtime", + denial: "write_not_allowed", + output: { isError: true }, + }); + expect(JSON.parse(denied.result_json).outcome).toBeUndefined(); + await child.stop(); + f.request.toolCallId = "next-read"; + f.request.modelToolName = f.snapshot.immutableBindings[0]!.tools.find( + (x) => x.tool.classification === "read", + )!.name; + child = await start(f, `mcp-test-${crypto.randomUUID()}`, undefined, false, 0, "2"); + await child.ready(); + await until("subsequent read", async () => { + const row = await callFor(f); + return row?.state === "succeeded" && row.completionEnqueuedVersion === row.version; + }); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await child?.stop(); + await system.query("delete from mcp_test.completions where session_id=$1", [f.sessionId]); + await f.cleanup(); + } + }, 30000); + +test("late verified outcome after expiry gets a new completion receipt without another effect", async () => { + const f = await effectFixture({ classification: "write" }); + let child: Awaited> | undefined; + try { + await approve(f); + child = await start(f, `mcp-test-${crypto.randomUUID()}`, "response_before_persist"); + await child.reached(); + const dispatched = (await callFor(f))!; + expect(dispatched.state).toBe("dispatching"); + expect((await f.command("stats")).calls).toBe(1); + await f.journal.expire(f.deploymentId, new Date(dispatched.intentExpiresAt!.getTime() + 1)); + const { makeMcpRecovery } = await import("../mcp/recovery.ts"); + const deliveries: unknown[] = []; + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + completer: { + async completeToolCall(req) { + deliveries.push(JSON.parse(req.resultJson)); + await system.query( + `insert into mcp_test.completions(session_id,call_id,result_json) values($1,$2,$3) on conflict(session_id,call_id) do update set attempts=mcp_test.completions.attempts+1`, + [req.sessionId, req.toolCallId, req.resultJson], + ); + }, + }, + }); + const marker = { + executionId: dispatched.id, + sessionId: f.sessionId, + runId: f.request.runId, + toolCallId: f.request.toolCallId, + }; + await runtime.runOnce(marker); + const unknown = (await callFor(f))!; + expect(unknown.state).toBe("outcome_unknown"); + expect(unknown.result).toBeNull(); + expect(deliveries).toMatchObject([ + { source: "runtime", outcome: "outcome_unknown", delivery: "unavailable" }, + ]); + expect(unknown.completionEnqueuedVersion).toBe(unknown.version); + f.requests.set("new-write", { ...f.request, toolCallId: "new-write" }); + await expect(f.driver.acceptToolRequest(f.requests.get("new-write")!)).rejects.toThrow(); + await f.journal.resolveUnknown(dispatched.id, f.actorId, "acknowledged", new Date()); + const resolved = (await callFor(f))!; + expect(resolved.state).toBe("outcome_unknown"); + expect(resolved.result).toBeNull(); + expect(resolved.version).toBe(unknown.version); + child.continue(); + await until("late completion", async () => { + const row = (await callFor(f))!; + return row.state === "succeeded" && row.completionEnqueuedVersion === row.version; + }); + const known = (await callFor(f))!; + expect(known.version).toBeGreaterThan(unknown.version); + expect(known.intentId).toBe(dispatched.intentId); + expect(known.result).toMatchObject({ outcome: "succeeded", delivery: "available" }); + const queued = ( + await system.query( + "select result_json,attempts from mcp_test.completions where session_id=$1 and call_id=$2", + [f.sessionId, f.request.toolCallId], + ) + ).rows[0]; + expect(queued.attempts).toBeGreaterThanOrEqual(2); + expect(JSON.parse(queued.result_json)).toMatchObject({ + source: "runtime", + outcome: "outcome_unknown", + }); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await child?.stop(); + await system.query("delete from mcp_test.completions where session_id=$1", [f.sessionId]); + await f.cleanup(); + } +}, 30000); + +for (const classification of ["read", "write"] as const) + test(`kill after provider send expires unknown and never repeats the ${classification}`, async () => { + const f = await effectFixture({ classification, response: "hold" }); + const executor = `mcp-test-${crypto.randomUUID()}`; + let child: Awaited> | undefined; + try { + if (classification === "write") await approve(f); + child = await start(f, executor); + await child.ready(); + await until("provider tools/call", async () => (await f.command("stats")).calls === 1); + const sent = (await callFor(f))!; + expect(sent.state).toBe("dispatching"); + await child.stop(); + await f.command("release"); + child = await start(f, executor, undefined, true, 61000); + await child.ready(); + await until("unknown completion", async () => { + const row = (await callFor(f))!; + return row.state === "outcome_unknown" && row.completionEnqueuedVersion === row.version; + }); + expect((await f.command("stats")).calls).toBe(1); + expect((await callFor(f))!.intentId).toBe(sent.intentId); + } finally { + await child?.stop(); + await f.command("release"); + await system.query("delete from mcp_test.completions where session_id=$1", [f.sessionId]); + await f.cleanup(); + } + }, 30000); + +for (const cause of ["policy", "invalid_json"] as const) + for (const boundary of ["before_enqueue", "enqueue_before_ack"]) + test(`durable ${cause} denial survives ${boundary} and changed request policy`, async () => { + const f = await effectFixture({ classification: "write", eligibility: "plan" }); + if (cause === "invalid_json") f.request.originalArguments = "{private-invalid-arguments"; + const executor = `mcp-test-${crypto.randomUUID()}`; + const originalRun = f.request.runId; + let child: Awaited> | undefined; + try { + child = await start(f, executor, boundary); + await child.reached(); + expect(await callFor(f)).toBeUndefined(); + await child.stop(); + // Both policy and the duplicate event change after the negative decision commits. + f.remote.eligibility = "write_eligible"; + f.setEligibility("write_eligible"); + f.request.originalArguments = '{"x":1}'; + f.request.runId = "changed-run"; + child = await start(f, executor, undefined, false, 0, "2"); + await child.ready(); + await until( + "denial redelivery", + async () => + ( + await system.query( + "select attempts from mcp_test.completions where session_id=$1 and call_id=$2", + [f.sessionId, f.request.toolCallId], + ) + ).rows[0]?.attempts >= (boundary === "enqueue_before_ack" ? 2 : 1), + ); + expect(await callFor(f)).toBeUndefined(); + expect((await f.command("stats")).calls).toBe(0); + const workflowId = `mcp-request:${f.sessionId}:${f.request.toolCallId}`; + const state = ( + await system.query("select inputs from dbos.workflow_status where workflow_uuid=$1", [ + workflowId, + ]) + ).rows[0]; + expect(JSON.parse(state.inputs).json).toEqual([ + { + sessionId: f.sessionId, + runId: originalRun, + toolCallId: f.request.toolCallId, + requestEventIdx: "1", + deadlineMs: expect.any(Number), + }, + ]); + const steps = ( + await system.query( + "select output,error from dbos.operation_outputs where workflow_uuid=$1 and function_name='mcp-request-decision'", + [workflowId], + ) + ).rows; + expect(steps).toHaveLength(1); + expect(JSON.parse(steps[0].output).json).toEqual({ + kind: "denied", + code: cause === "policy" ? "write_not_allowed" : "invalid_arguments", + }); + expect(steps[0].error).toBeNull(); + expect(JSON.stringify(steps)).not.toContain("private-invalid-arguments"); + expect(JSON.stringify(steps)).not.toContain("known-literal-token"); + } finally { + await child?.stop(); + await system.query("delete from mcp_test.completions where session_id=$1", [f.sessionId]); + await f.cleanup(); + } + }, 30000); + +test("transient request read retries with only a fixed error in DBOS", async () => { + const f = await effectFixture(); + const executor = `mcp-test-${crypto.randomUUID()}`; + let child: Awaited> | undefined; + try { + child = await start(f, executor, undefined, false, 0, "1", 1); + await child.ready(); + await until("retry completes", async () => { + const row = await callFor(f); + return row?.state === "succeeded" && row.completionEnqueuedVersion === row.version; + }); + const steps = ( + await system.query( + "select output,error from dbos.operation_outputs where workflow_uuid=$1 and function_name='mcp-request-decision' order by function_id", + [`mcp-request:${f.sessionId}:${f.request.toolCallId}`], + ) + ).rows; + expect(steps).toHaveLength(2); + expect(steps[0].error).toContain("MCP request acceptance unavailable"); + expect(steps[1].error).toBeNull(); + expect(JSON.stringify(steps)).not.toContain("private-control-error"); + expect(JSON.stringify(steps)).not.toContain("known-literal-token"); + expect(JSON.stringify(steps)).not.toContain("originalArguments"); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await child?.stop(); + await system.query("delete from mcp_test.completions where session_id=$1", [f.sessionId]); + await f.cleanup(); + } +}, 30000); + +test("request decision restart keeps its fixed deadline and expires without an effect", async () => { + const f = await effectFixture(); + const executor = `mcp-test-${crypto.randomUUID()}`; + const workflowId = `mcp-request:${f.sessionId}:${f.request.toolCallId}`; + let child: Awaited> | undefined; + try { + child = await start(f, executor, "request_read_failed", false, 0, "1", 1); + await child.reached(); + const first = ( + await system.query("select inputs from dbos.workflow_status where workflow_uuid=$1", [ + workflowId, + ]) + ).rows[0].inputs; + expect(JSON.parse(first).json[0].deadlineMs).toBeNumber(); + await child.stop(); + child = await start(f, executor, undefined, false, 931000); + await child.ready(); + await until( + "expired denial", + async () => + !!( + await system.query( + "select 1 from mcp_test.completions where session_id=$1 and call_id=$2", + [f.sessionId, f.request.toolCallId], + ) + ).rowCount, + ); + expect(await callFor(f)).toBeUndefined(); + expect((await f.command("stats")).calls).toBe(0); + const completion = ( + await system.query( + "select result_json from mcp_test.completions where session_id=$1 and call_id=$2", + [f.sessionId, f.request.toolCallId], + ) + ).rows[0]; + expect(JSON.parse(completion.result_json).denial).toBe("deadline_expired"); + const restored = ( + await system.query("select inputs from dbos.workflow_status where workflow_uuid=$1", [ + workflowId, + ]) + ).rows[0].inputs; + expect(restored).toBe(first); + } finally { + await child?.stop(); + await system.query("delete from mcp_test.completions where session_id=$1", [f.sessionId]); + await f.cleanup(); + } +}, 30000); + +test("changed duplicate after accepted completion retains the first workflow and result", async () => { + const f = await effectFixture(); + let child: Awaited> | undefined; + try { + child = await start(f, `mcp-test-${crypto.randomUUID()}`); + await child.ready(); + await until("first receipt", async () => { + const row = await callFor(f); + return row?.state === "succeeded" && row.completionEnqueuedVersion === row.version; + }); + const first = (await callFor(f))!; + const delivered = ( + await system.query( + "select result_json,attempts from mcp_test.completions where session_id=$1 and call_id=$2", + [f.sessionId, f.request.toolCallId], + ) + ).rows[0]; + await child.stop(); + f.request.runId = "changed-run"; + f.request.originalArguments = "{invalid-duplicate}"; + child = await start(f, `mcp-test-${crypto.randomUUID()}`, undefined, false, 0, "2"); + await child.ready(); + expect(await callFor(f)).toEqual(first); + expect( + ( + await system.query( + "select result_json,attempts from mcp_test.completions where session_id=$1 and call_id=$2", + [f.sessionId, f.request.toolCallId], + ) + ).rows[0], + ).toEqual(delivered); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await child?.stop(); + await system.query("delete from mcp_test.completions where session_id=$1", [f.sessionId]); + await f.cleanup(); + } +}, 30000); diff --git a/orchestrator/src/__tests__/mcp-effect-fixture.ts b/orchestrator/src/__tests__/mcp-effect-fixture.ts index f935d453f..aa15b7615 100644 --- a/orchestrator/src/__tests__/mcp-effect-fixture.ts +++ b/orchestrator/src/__tests__/mcp-effect-fixture.ts @@ -2,7 +2,7 @@ import { expect } from "bun:test"; import { spawn } from "node:child_process"; import { createInterface } from "node:readline"; import { once } from "node:events"; -import { create } from "@bufbuild/protobuf"; +import { create, toJson } from "@bufbuild/protobuf"; import { createClient, createRouterTransport } from "@connectrpc/connect"; import { eq } from "drizzle-orm"; import { workerTls } from "../../../mcp-worker/test/fixtures/worker-tls.ts"; @@ -22,7 +22,10 @@ import { CredentialStatusSchema, } from "../gen/engram/mcp/v1/control_pb.ts"; import { SessionPolicyService } from "../gen/engram/mcp/v1/session_policy_pb.ts"; -import type { McpWritePolicyRef } from "../gen/engram/app/v1/session_pb.ts"; +import { + McpWritePolicyRefSchema, + type McpWritePolicyRef, +} from "../gen/engram/app/v1/session_pb.ts"; import { statusMetadata } from "../mcp/credential-store.ts"; import { unwrapCredentialControlClient } from "../mcp/control-client.ts"; import { makeMcpEffectDriver } from "../mcp/effect-driver.ts"; @@ -140,6 +143,7 @@ export async function effectFixture( token = "known-literal-token", leases = 0; let enabled = true; + let authorityFailures = 0; let clockOffsetMs = 0; let beforeFence: (() => Promise) | undefined; let beforeLease: (() => Promise) | undefined; @@ -256,7 +260,13 @@ export async function effectFixture( const resolver = makeStoredSessionBindingResolver( { control: policy, - readOperatorPolicies: async () => operatorPolicies, + readOperatorPolicies: async () => { + if (authorityFailures > 0) { + authorityFailures--; + throw Error("private-authority-transport-error"); + } + return operatorPolicies; + }, enforceManagedDeny: async (expected) => expected, }, f.db, @@ -278,6 +288,17 @@ export async function effectFixture( return { ...f, driver, + remote: { + workerTls: { + ...tls, + url: `https://127.0.0.1:${ready.port}`, + serverFingerprint: tls.fingerprint, + }, + operatorPolicies, + status: toJson(CredentialStatusSchema, status), + eligibility, + policyRef: toJson(McpWritePolicyRefSchema, f.input.requestPolicyContext.acceptedPolicyRef), + }, resolver, request, requests, @@ -289,6 +310,9 @@ export async function effectFixture( advanceNow(milliseconds: number) { clockOffsetMs += milliseconds; }, + failAuthorityReads(count: number) { + authorityFailures = count; + }, setEnabled(value: boolean) { enabled = value; }, diff --git a/orchestrator/src/__tests__/mcp-recovery.test.ts b/orchestrator/src/__tests__/mcp-recovery.test.ts new file mode 100644 index 000000000..69c636174 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-recovery.test.ts @@ -0,0 +1,496 @@ +import { expect, test } from "bun:test"; +import { Code, ConnectError } from "@connectrpc/connect"; +import { eq } from "drizzle-orm"; +import { effectFixture } from "./mcp-effect-fixture.ts"; +import { mcpExecution } from "../db/schema.ts"; +import { makeMcpRecovery } from "../mcp/recovery.ts"; + +import { verifyMcpTestDatabases } from "./helpers/mcp-test-isolation.ts"; +await verifyMcpTestDatabases(); + +test("committed result is redelivered after enqueue failure without another effect", async () => { + const f = await effectFixture(); + try { + let fail = true; + const delivered: string[] = []; + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + completer: { + async completeToolCall(input) { + if (fail) throw Error("transport failure"); + delivered.push(input.resultJson); + }, + }, + }); + const marker = await runtime.accept(f.request); + await expect(runtime.runOnce(marker)).rejects.toThrow("MCP completion unavailable"); + const committed = (await f.journal.get(marker.executionId))!; + expect(committed.state).toBe("succeeded"); + expect(committed.completionEnqueuedVersion).toBe(0); + fail = false; + expect(await runtime.runOnce(marker)).toBe("done"); + expect(JSON.parse(delivered[0]!)).toEqual({ ...committed.result, resultUnavailable: false }); + expect((await f.journal.get(marker.executionId))!.completionEnqueuedVersion).toBe( + committed.version, + ); + expect(await runtime.runOnce(marker)).toBe("done"); + expect(delivered).toHaveLength(1); + expect((await f.command("stats")).calls).toBe(1); + expect((await runtime.pending()).map((x) => x.executionId)).not.toContain(marker.executionId); + await expect( + f.db + .update(mcpExecution) + .set({ completionEnqueuedVersion: 0 }) + .where(eq(mcpExecution.id, marker.executionId)) + .execute(), + ).rejects.toThrow(); + await expect( + f.db + .update(mcpExecution) + .set({ completionEnqueuedVersion: committed.version + 1 }) + .where(eq(mcpExecution.id, marker.executionId)) + .execute(), + ).rejects.toThrow(); + } finally { + await f.cleanup(); + } +}, 20000); + +test("recovery expires unadmitted rows and delivers a local cancellation", async () => { + const f = await effectFixture({ classification: "write" }); + try { + let now = new Date(); + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => now, + completer: { + async completeToolCall(input) { + expect(JSON.parse(input.resultJson)).toMatchObject({ + source: "runtime", + outcome: "canceled", + delivery: "unavailable", + }); + }, + }, + }); + const marker = await runtime.accept(f.request); + const call = (await f.journal.get(marker.executionId))!; + now = new Date(call.deadline.getTime() + 1); + const { makeMcpRecoveryScanner } = await import("../mcp/recovery-scanner.ts"); + await makeMcpRecoveryScanner({ + deploymentId: f.deploymentId, + journal: f.journal, + runtime: () => undefined, + now: () => now, + }).runOnce(); + expect((await f.journal.get(call.id))!.state).toBe("canceled"); + expect((await runtime.pending()).map((x) => x.executionId)).toContain(call.id); + expect(await runtime.runOnce(marker)).toBe("done"); + expect((await runtime.pending()).map((x) => x.executionId)).not.toContain(call.id); + } finally { + await f.cleanup(); + } +}, 20000); + +for (const response of [ + "error", + "image", + "normalized-limit", + "malformed", + "oversize", + "local_validation", +]) + test(`completion retains the committed ${response} distinction`, async () => { + const f = await effectFixture({ + response: response === "local_validation" ? "normal" : response, + }); + try { + if (response === "local_validation") f.request.originalArguments = '{"x":"wrong"}'; + let result: unknown; + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + completer: { + async completeToolCall(request) { + result = JSON.parse(request.resultJson); + }, + }, + }); + const marker = await runtime.accept(f.request); + expect(await runtime.runOnce(marker)).toBe("done"); + const call = (await f.journal.get(marker.executionId))!; + expect(result).toEqual({ ...call.result, resultUnavailable: call.resultUnavailable }); + if (response === "local_validation") { + expect(result).toMatchObject({ + source: "local_validation", + delivery: "available", + output: { isError: true }, + }); + expect(call.intentId).toBeNull(); + expect((await f.command("stats")).calls).toBe(0); + } else { + expect((await f.command("stats")).calls).toBe(1); + if (response === "error") + expect(result).toMatchObject({ outcome: "failed", output: { isError: true } }); + if (response === "image" || response === "normalized-limit") + expect(result).toMatchObject({ + outcome: "succeeded", + delivery: "unavailable", + resultUnavailable: true, + }); + } + await runtime.runOnce(marker); + expect(JSON.stringify(result)).not.toContain("known-literal-token"); + } finally { + await f.cleanup(); + } + }, 20000); + +test("bounded keyset scans reach calls behind a full page and stop after shutdown", async () => { + const f = await effectFixture(); + try { + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + completer: { + async completeToolCall() { + throw Error("not run"); + }, + }, + }); + const expected = new Set(); + for (let i = 0; i < 130; i++) { + const request = { ...f.request, toolCallId: `queued-${i}` }; + f.requests.set(request.toolCallId, request); + expected.add((await runtime.accept(request)).executionId); + } + const { makeMcpRecoveryScanner } = await import("../mcp/recovery-scanner.ts"); + const scheduled = new Set(); + let calls = 0; + const scanner = makeMcpRecoveryScanner({ + deploymentId: f.deploymentId, + journal: f.journal, + runtime: () => runtime, + startWorkflow: async (ref) => { + calls++; + expect(Object.keys(ref).sort()).toEqual([ + "executionId", + "runId", + "sessionId", + "toolCallId", + ]); + scheduled.add(ref.executionId); + }, + }); + await scanner.runOnce(); + expect(calls).toBe(128); + for (let i = 0; i < 4 && [...expected].some((id) => !scheduled.has(id)); i++) + await scanner.runOnce(); + expect([...expected].every((id) => scheduled.has(id))).toBe(true); + await scanner.start(); + await scanner.stop(); + const stopped = calls; + await Bun.sleep(1100); + expect(calls).toBe(stopped); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("already-completed guest projection cannot replace or remove journal truth", async () => { + const f = await effectFixture(); + try { + const call = await f.driver.acceptToolRequest(f.request); + const committed = await f.driver.driveOnce(call.id); + const { makePendingToolCallStore } = await import("../tools/pending-tool-calls.ts"); + const pending = makePendingToolCallStore(f.db); + await pending.recordRequested({ + sessionId: f.sessionId, + toolCallId: f.request.toolCallId, + toolName: f.request.modelToolName, + handling: "handled", + requestedAt: new Date(), + }); + await pending.markCompleted(f.sessionId, f.request.toolCallId, new Date()); + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + // The coordinator acknowledges and drops results for an already-completed call. + completer: { + async completeToolCall() { + return {}; + }, + }, + }); + await runtime.runOnce({ + executionId: call.id, + sessionId: f.sessionId, + runId: f.request.runId, + toolCallId: f.request.toolCallId, + }); + const retained = (await f.journal.get(call.id))!; + expect(retained.result).toEqual(committed.result); + expect(retained.state).toBe("succeeded"); + expect(retained.completionEnqueuedVersion).toBe(retained.version); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await f.cleanup(); + } +}, 20000); + +test("test topology rejects a missing or primary-looking DBOS URL before connection", async () => { + const saved = process.env.MCP_TEST_DBOS_DATABASE_URL; + try { + delete process.env.MCP_TEST_DBOS_DATABASE_URL; + await expect(verifyMcpTestDatabases()).rejects.toThrow( + "explicit isolated MCP test databases required", + ); + process.env.MCP_TEST_DBOS_DATABASE_URL = "postgresql://127.0.0.1:5435/engram_orchestrator"; + await expect(verifyMcpTestDatabases()).rejects.toThrow("MCP test database isolation mismatch"); + } finally { + process.env.MCP_TEST_DBOS_DATABASE_URL = saved; + } +}); + +test("permanent input and policy denials have explicit typed codes", async () => { + const f = await effectFixture({ classification: "write", eligibility: "plan" }); + try { + await expect(f.driver.acceptToolRequest(f.request)).rejects.toMatchObject({ + code: "write_not_allowed", + }); + await expect( + f.driver.acceptToolRequest({ ...f.request, originalArguments: "{broken" }), + ).rejects.toMatchObject({ code: "invalid_arguments" }); + } finally { + await f.cleanup(); + } +}, 20000); + +test("request decision uses the exact int64 event and preserves first arguments and identity", async () => { + const f = await effectFixture(); + try { + const index = 9007199254740993n; + let fail = true; + let reads = 0; + const original = ' {"x": 1} '; + f.request.originalArguments = original; + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + async readRequestEvent(sessionId, idx) { + expect(sessionId).toBe(f.sessionId); + expect(idx).toBe(index); + reads++; + if (fail) throw Error("private-control-error"); + return { + idx, + kind: "tool_call_requested", + payloadJson: JSON.stringify({ + tool_call_id: f.request.toolCallId, + run_id: f.request.runId, + name: f.request.modelToolName, + args_json: original, + }), + }; + }, + }); + const ref = { + sessionId: f.sessionId, + runId: f.request.runId, + toolCallId: f.request.toolCallId, + requestEventIdx: String(index), + deadlineMs: Date.now() + 930000, + }; + await expect(runtime.decideRequest(ref)).rejects.toThrow("MCP request acceptance unavailable"); + fail = false; + const first = await runtime.decideRequest(ref); + expect(first.kind).toBe("accepted"); + if (first.kind !== "accepted") throw Error("expected accepted references"); + const call = (await f.journal.get(first.execution.executionId))!; + expect(call.originalArguments).toBe(original); + const duplicate = await runtime.decideRequest({ + ...ref, + runId: "changed-run", + requestEventIdx: "12", + deadlineMs: 0, + }); + expect(duplicate).toEqual(first); + expect(reads).toBe(2); + expect((await f.journal.get(call.id))!.deadline).toEqual(call.deadline); + expect((await f.command("stats")).calls).toBe(0); + expect(JSON.stringify(first)).not.toContain("originalArguments"); + } finally { + await f.cleanup(); + } +}); + +test("request decision rejects mismatched event references with a fixed private error", async () => { + const f = await effectFixture(); + try { + let kind = "tool_call_requested", + idx = 1n, + run = f.request.runId, + call = f.request.toolCallId; + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + async readRequestEvent() { + return { + idx, + kind, + payloadJson: JSON.stringify({ + run_id: run, + tool_call_id: call, + name: f.request.modelToolName, + args_json: f.request.originalArguments, + }), + }; + }, + }); + const ref = { + sessionId: f.sessionId, + runId: f.request.runId, + toolCallId: f.request.toolCallId, + requestEventIdx: "1", + deadlineMs: Date.now() + 930000, + }; + for (const bad of ["0", "-1", "1.1", "9223372036854775808"]) + await expect(runtime.decideRequest({ ...ref, requestEventIdx: bad })).rejects.toThrow( + "MCP request acceptance unavailable", + ); + idx = 2n; + await expect(runtime.decideRequest(ref)).rejects.toThrow("MCP request acceptance unavailable"); + idx = 1n; + kind = "tool_call_completed"; + await expect(runtime.decideRequest(ref)).rejects.toThrow("MCP request acceptance unavailable"); + kind = "tool_call_requested"; + run = "changed-run"; + await expect(runtime.decideRequest(ref)).rejects.toThrow("MCP request acceptance unavailable"); + run = f.request.runId; + call = "changed-call"; + await expect(runtime.decideRequest(ref)).rejects.toThrow("MCP request acceptance unavailable"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.cleanup(); + } +}); + +test("transient binding authority failure is retried while a verified absent tool is denied", async () => { + const f = await effectFixture(); + try { + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + async readRequestEvent(_sessionId, idx) { + return { + idx, + kind: "tool_call_requested", + payloadJson: JSON.stringify({ + run_id: f.request.runId, + tool_call_id: f.request.toolCallId, + name: f.request.modelToolName, + args_json: f.request.originalArguments, + }), + }; + }, + }); + const ref = { + sessionId: f.sessionId, + runId: f.request.runId, + toolCallId: f.request.toolCallId, + requestEventIdx: "1", + deadlineMs: Date.now() + 930000, + }; + f.failAuthorityReads(1); + await expect(runtime.decideRequest(ref)).rejects.toThrow("MCP request acceptance unavailable"); + expect((await runtime.decideRequest(ref)).kind).toBe("accepted"); + f.request.toolCallId = "absent-tool"; + f.request.modelToolName = "mcp_unpublished_tool"; + expect( + await runtime.decideRequest({ + ...ref, + toolCallId: f.request.toolCallId, + requestEventIdx: "2", + }), + ).toEqual({ kind: "denied", code: "tool_unavailable" }); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.cleanup(); + } +}); + +for (const disposition of ["missing", "terminal", "host_lost", "conflict", "transport"] as const) + test(`completion ${disposition} settles only authoritative target loss`, async () => { + const f = await effectFixture(); + try { + let fail = true; + const error = new ConnectError( + "private-delivery-error", + disposition === "missing" + ? Code.NotFound + : disposition === "transport" + ? Code.Unavailable + : Code.FailedPrecondition, + new Headers({ + "engram-error-slug": disposition === "terminal" ? "snapshot_invalidated" : disposition, + }), + ); + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + completer: { + async completeToolCall() { + if (fail) throw error; + }, + }, + }); + const marker = await runtime.accept(f.request); + const ref = { + sessionId: f.sessionId, + runId: f.request.runId, + toolCallId: f.request.toolCallId, + requestEventIdx: "1", + deadlineMs: Date.now() + 930000, + }; + if (disposition === "missing" || disposition === "terminal") { + expect(await runtime.runOnce(marker)).toBe("done"); + await runtime.deliverDenial(ref, "write_not_allowed"); + } else { + await expect(runtime.runOnce(marker)).rejects.toThrow("MCP completion unavailable"); + await expect(runtime.deliverDenial(ref, "write_not_allowed")).rejects.toThrow( + "MCP denial completion unavailable", + ); + expect((await f.journal.get(marker.executionId))!.completionEnqueuedVersion).toBe(0); + expect((await runtime.pending()).map((x) => x.executionId)).toContain(marker.executionId); + fail = false; + expect(await runtime.runOnce(marker)).toBe("done"); + } + const settled = (await f.journal.get(marker.executionId))!; + expect(settled.state).toBe("succeeded"); + expect(settled.completionEnqueuedVersion).toBe(settled.version); + expect((await runtime.pending()).map((x) => x.executionId)).not.toContain(marker.executionId); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await f.cleanup(); + } + }, 20000); diff --git a/orchestrator/src/db/schema.ts b/orchestrator/src/db/schema.ts index 1cddf551d..e74a43f0f 100644 --- a/orchestrator/src/db/schema.ts +++ b/orchestrator/src/db/schema.ts @@ -175,8 +175,8 @@ export const taskSession = pgTable( // mutable workflow state. integrationPrincipalId: text("integration_principal_id"), // Candidate metadata only. The host must separately acknowledge deny enforcement. - mcpBindingEnvelope: jsonb("mcp_binding_envelope") - .$type(), + mcpBindingEnvelope: + jsonb("mcp_binding_envelope").$type(), // ADR 0109: content-hash of the compiled authorization snapshot // (profileId + grants + connections). The broker emits it as the // `engrams_profile_snapshot` OIDC claim, so Google-side audit logs join @@ -1068,7 +1068,6 @@ export const consumerCursor = pgTable( (t) => [primaryKey({ columns: [t.sessionId, t.consumer] })], ); - // --------------------------------------------------------------------------- // DBOS orphan sweep (ADR 0104) // --------------------------------------------------------------------------- @@ -1194,65 +1193,62 @@ export const DEFAULT_PROFILE_NETWORK: ProfileNetwork = { allowHostPatterns: [], }; -export const profile = pgTable( - "profile", - { - id: text("id").primaryKey(), // uuid string (crypto.randomUUID()) - name: text("name").notNull(), - description: text("description").notNull().default(""), - icon: text("icon").notNull().default("Bot"), // lucide icon name - imageId: text("image_id").notNull(), // logical ref → enabled_images.id (§3) - // ADR 0062/0063: the default harness (a HarnessCatalogService catalog name) - // this profile's sessions run, with default model + effort (catalog option - // ids). `harness` is REQUIRED — a profile always names a concrete harness (the - // "inherit deployment default" semantics were superseded; existing rows were - // backfilled to `claude`). model/effort stay nullable → the harness - // descriptor's defaults. All overridable per session. - harness: text("harness").notNull(), - modelRouter: text("model_router"), - model: text("model"), - effort: text("effort"), - programmaticOnly: boolean("programmatic_only").notNull().default(false), - includeUserTokens: boolean("include_user_tokens").notNull().default(false), - envVars: jsonb("env_vars").notNull().default({}), // { KEY: VALUE } - // ADR 0055: dynamic skill bundle names this profile's sessions mount (e.g. - // ["skills", "browser"]). Resolved by the coordinator to reserved-slot - // mounts at session create. Empty = base session (no skills). - skills: jsonb("skills").$type().notNull().default([]), - // ADR 0109: structured integration authority. Unlike the retired flat - // capability array, this preserves which configured identity an operation - // uses and keeps its resource constraints attached. - mcpGrantPublication: jsonb("mcp_grant_publication").$type<{ - revision: number; - actorId: string; - grantsHash: string; - }>(), - integrationGrants: jsonb("integration_grants") - .$type() - .notNull() - .default([]), - // ADR 0057: egress network allow-list (deny by default) + secrets this - // profile's sessions get, lifted off the image manifest. Additive in B1; - // compiled into the per-session SessionPolicy + consumed at boot in B2. - network: jsonb("network").$type().notNull().default(DEFAULT_PROFILE_NETWORK), - secrets: jsonb("secrets").$type().notNull().default([]), - // Git repositories this profile's image contains — explicit, user-managed - // (by hand or via DiscoverProfileRepos). Fed to the routing picker's cards. - repos: jsonb("repos").$type().notNull().default([]), - // ADR 0118: the services every session from this profile hosts. The - // orchestrator reserves one hostname per app BEFORE it creates the session - // (inside the transaction below this one) and injects each app's address - // into the guest's env, so a sibling app is configured with an address the - // user's browser can reach too. Empty = no apps. - apps: jsonb("apps").$type().notNull().default([]), - createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at") - .notNull() - .defaultNow() - .$onUpdate(() => new Date()), - deletedAt: timestamp("deleted_at"), // null = active; soft delete only (§4) - }, -); +export const profile = pgTable("profile", { + id: text("id").primaryKey(), // uuid string (crypto.randomUUID()) + name: text("name").notNull(), + description: text("description").notNull().default(""), + icon: text("icon").notNull().default("Bot"), // lucide icon name + imageId: text("image_id").notNull(), // logical ref → enabled_images.id (§3) + // ADR 0062/0063: the default harness (a HarnessCatalogService catalog name) + // this profile's sessions run, with default model + effort (catalog option + // ids). `harness` is REQUIRED — a profile always names a concrete harness (the + // "inherit deployment default" semantics were superseded; existing rows were + // backfilled to `claude`). model/effort stay nullable → the harness + // descriptor's defaults. All overridable per session. + harness: text("harness").notNull(), + modelRouter: text("model_router"), + model: text("model"), + effort: text("effort"), + programmaticOnly: boolean("programmatic_only").notNull().default(false), + includeUserTokens: boolean("include_user_tokens").notNull().default(false), + envVars: jsonb("env_vars").notNull().default({}), // { KEY: VALUE } + // ADR 0055: dynamic skill bundle names this profile's sessions mount (e.g. + // ["skills", "browser"]). Resolved by the coordinator to reserved-slot + // mounts at session create. Empty = base session (no skills). + skills: jsonb("skills").$type().notNull().default([]), + // ADR 0109: structured integration authority. Unlike the retired flat + // capability array, this preserves which configured identity an operation + // uses and keeps its resource constraints attached. + mcpGrantPublication: jsonb("mcp_grant_publication").$type<{ + revision: number; + actorId: string; + grantsHash: string; + }>(), + integrationGrants: jsonb("integration_grants") + .$type() + .notNull() + .default([]), + // ADR 0057: egress network allow-list (deny by default) + secrets this + // profile's sessions get, lifted off the image manifest. Additive in B1; + // compiled into the per-session SessionPolicy + consumed at boot in B2. + network: jsonb("network").$type().notNull().default(DEFAULT_PROFILE_NETWORK), + secrets: jsonb("secrets").$type().notNull().default([]), + // Git repositories this profile's image contains — explicit, user-managed + // (by hand or via DiscoverProfileRepos). Fed to the routing picker's cards. + repos: jsonb("repos").$type().notNull().default([]), + // ADR 0118: the services every session from this profile hosts. The + // orchestrator reserves one hostname per app BEFORE it creates the session + // (inside the transaction below this one) and injects each app's address + // into the guest's env, so a sibling app is configured with an address the + // user's browser can reach too. Empty = no apps. + apps: jsonb("apps").$type().notNull().default([]), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at") + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + deletedAt: timestamp("deleted_at"), // null = active; soft delete only (§4) +}); // --------------------------------------------------------------------------- // Automations (ADR 0102 → ADR 0119): versioned block-graph definitions whose @@ -1312,7 +1308,10 @@ export const routerModel = pgTable( available: boolean("available").notNull().default(true), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), }, - (t) => [primaryKey({ columns: [t.routerId, t.modelId] }), index("router_model_available_idx").on(t.routerId, t.available)], + (t) => [ + primaryKey({ columns: [t.routerId, t.modelId] }), + index("router_model_available_idx").on(t.routerId, t.available), + ], ); export const routerModelPolicy = pgTable( @@ -1326,7 +1325,10 @@ export const routerModelPolicy = pgTable( }, (t) => [ primaryKey({ columns: [t.routerId, t.modelId] }), - foreignKey({ columns: [t.routerId, t.modelId], foreignColumns: [routerModel.routerId, routerModel.modelId] }).onDelete("cascade"), + foreignKey({ + columns: [t.routerId, t.modelId], + foreignColumns: [routerModel.routerId, routerModel.modelId], + }).onDelete("cascade"), check("router_model_policy_user_implies_enabled", sql`NOT ${t.userEnabled} OR ${t.enabled}`), ], ); @@ -2230,7 +2232,8 @@ export const mcpExecution = pgTable( requestDigest: text("request_digest").notNull(), argumentHash: text("argument_hash").notNull(), originalArguments: text("original_arguments"), - requestContext: jsonb("request_context").$type(), + requestContext: + jsonb("request_context").$type(), upstreamName: text("upstream_name").notNull(), configRevision: integer("config_revision").notNull(), authGeneration: integer("auth_generation").notNull(), @@ -2247,6 +2250,8 @@ export const mcpExecution = pgTable( deadline: timestamp("deadline", { withTimezone: true }).notNull(), result: jsonb("result").$type>(), resultUnavailable: boolean("result_unavailable").notNull().default(false), + // Delivery settlement: enqueue ACK or authoritative target-gone rejection, never guest/effect proof. + completionEnqueuedVersion: integer("completion_enqueued_version").notNull().default(0), createdAt: timestamp("created_at", { withTimezone: true }).notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(), }, @@ -2260,6 +2265,13 @@ export const mcpExecution = pgTable( index("mcp_execution_connection_state_idx").on(t.connectionId, t.sessionId, t.state), index("mcp_execution_catalog_idx").on(t.catalogId), index("mcp_execution_pending_idx").on(t.state, t.deadline), + index("mcp_execution_recovery_idx") + .on(t.id) + .where(sql`${t.completionEnqueuedVersion} < ${t.version}`), + check( + "mcp_execution_completion_version_check", + sql`${t.completionEnqueuedVersion} >= 0 AND ${t.completionEnqueuedVersion} <= ${t.version}`, + ), uniqueIndex("mcp_execution_intent_idx").on(t.intentId), check( "mcp_execution_state_check", @@ -2332,7 +2344,9 @@ export const mcpAdmission = pgTable( executionId: text("execution_id") .primaryKey() .references(() => mcpExecution.id, { onDelete: "cascade" }), - reservationId: text("reservation_id").notNull().default(sql`gen_random_uuid()::text`), + reservationId: text("reservation_id") + .notNull() + .default(sql`gen_random_uuid()::text`), deploymentId: text("deployment_id") .notNull() .references(() => mcpAdmissionScope.id), diff --git a/orchestrator/src/index.ts b/orchestrator/src/index.ts index 450ea6cc1..b7283dac5 100644 --- a/orchestrator/src/index.ts +++ b/orchestrator/src/index.ts @@ -71,6 +71,8 @@ import type { ConnectRouter } from "@connectrpc/connect"; // ABOVE the initDbos() call below so their workflows/steps are registered // before DBOS.launch(). Imports below register the finite Slack-thread and // tool-execution workflows. +import "./workflows/mcp-exec.ts"; +import { makeMcpRecoveryScanner } from "./mcp/recovery-scanner.ts"; import { initDbos, shutdownDbos } from "./workflows/dbos.ts"; import { startSpecTicketSyncWorkflow } from "./workflows/spec-ticket-sync.ts"; import { makeProductionListenerManager } from "./listeners/manager.ts"; @@ -570,16 +572,24 @@ const server = buildServer( // to the control plane with per-method CASL authz gate (ADR 0051 Task 18). // ADR 0053 §3: a DisableImage pre-flight blocks disabling an image that // any active profile still references (the coordinator only knows sessions). - registerPassthrough(router, SURFACE, controlPlaneTransport, undefined, undefined, { - "ImageService.DisableImage": makeDisableImageGuard(), - "SessionService.CompleteToolCall": makeExternalToolCompletionGuard(), - "SessionService.SendPrompt": async (request) => { - const sessionId = (request as { sessionId?: unknown }).sessionId; - if (typeof sessionId !== "string") return; - const response = await controlPlaneSessions.getSession({ sessionId }); - await productionSpecProjection.preparePrompt(sessionId, response.session?.status ?? ""); + registerPassthrough( + router, + SURFACE, + controlPlaneTransport, + undefined, + undefined, + { + "ImageService.DisableImage": makeDisableImageGuard(), + "SessionService.CompleteToolCall": makeExternalToolCompletionGuard(), + "SessionService.SendPrompt": async (request) => { + const sessionId = (request as { sessionId?: unknown }).sessionId; + if (typeof sessionId !== "string") return; + const response = await controlPlaneSessions.getSession({ sessionId }); + await productionSpecProjection.preparePrompt(sessionId, response.session?.status ?? ""); + }, }, - }, makeSessionPolicyOverrides()); + makeSessionPolicyOverrides(), + ); }, // Raw WS-upgrade hooks, tried in order: the preview proxy first // (Host-keyed — a preview host is a different origin, so it wins @@ -660,6 +670,8 @@ void retireProviderWebhookSchemes({ }).catch((err) => log.error({ err }, "provider webhook scheme retirement failed")); if (process.env.ENGRAM_DEV_TOOLS === "1") registerDevTools(); await initDbos(); +const mcpRecoveryScanner = makeMcpRecoveryScanner({ deploymentId: config.deploymentId }); +await mcpRecoveryScanner.start(); assertSweepPoliciesExhaustive(); // ADR 0119 D1: the block registry is static and must be populated before the // first RPC validates a definition — registration was lazy (first interpreter @@ -732,6 +744,7 @@ process.on("SIGTERM", () => { await automationScheduler.stop(); await integrationEventSweeper.stop(); await listenerManager.stop(); + await mcpRecoveryScanner.stop(); await specPublishScanner.stop(); await draftingSeedScanner.stop(); await specSyncHub.stop(); diff --git a/orchestrator/src/listeners/__tests__/tool-consumer.test.ts b/orchestrator/src/listeners/__tests__/tool-consumer.test.ts index 81edf7391..45adb3d29 100644 --- a/orchestrator/src/listeners/__tests__/tool-consumer.test.ts +++ b/orchestrator/src/listeners/__tests__/tool-consumer.test.ts @@ -3,14 +3,8 @@ import { z } from "zod"; import { registerBuiltinTools } from "../../tools/builtin.ts"; import { createToolRegistry } from "../../tools/registry.ts"; -import type { - PendingToolCallInput, - PendingToolCallStore, -} from "../../tools/pending-tool-calls.ts"; -import { - makeToolConsumer, - type ToolWorkflowStarter, -} from "../tool-consumer.ts"; +import type { PendingToolCallInput, PendingToolCallStore } from "../../tools/pending-tool-calls.ts"; +import { makeToolConsumer, type ToolWorkflowStarter } from "../tool-consumer.ts"; function pendingRecorder() { const requested: PendingToolCallInput[] = []; @@ -185,22 +179,28 @@ describe("tool consumer", () => { }); await consumer.handle(requested("ask_user_question"), { sessionId: "session-1" }); - await consumer.handle({ - idx: 2n, - kind: "tool_result_submitted", - payloadJson: JSON.stringify({ - tool_call_id: "call-1", - at: "2026-07-13T12:01:00.000Z", - }), - }, { sessionId: "session-1" }); - await consumer.handle({ - idx: 3n, - kind: "tool_call_completed", - payloadJson: JSON.stringify({ - tool_call_id: "call-1", - at: "2026-07-13T12:02:00.000Z", - }), - }, { sessionId: "session-1" }); + await consumer.handle( + { + idx: 2n, + kind: "tool_result_submitted", + payloadJson: JSON.stringify({ + tool_call_id: "call-1", + at: "2026-07-13T12:01:00.000Z", + }), + }, + { sessionId: "session-1" }, + ); + await consumer.handle( + { + idx: 3n, + kind: "tool_call_completed", + payloadJson: JSON.stringify({ + tool_call_id: "call-1", + at: "2026-07-13T12:02:00.000Z", + }), + }, + { sessionId: "session-1" }, + ); expect(pending.requested[0]).toEqual({ sessionId: "session-1", @@ -225,3 +225,45 @@ describe("tool consumer", () => { ]); }); }); + +test("MCP requests persist the exact event reference before cursor success", async () => { + const pending = pendingRecorder(); + const accepted: unknown[] = []; + let fail = true; + const consumer = makeToolConsumer({ + registry: registryFixture(), + pendingCalls: pending.store, + now: () => new Date(0), + startWorkflow: async () => { + throw Error("static path used"); + }, + startMcpRequest: async (input) => { + accepted.push(input); + if (fail) throw Error("acceptance unavailable"); + }, + }); + const event = { + idx: 7n, + kind: "tool_call_requested", + payloadJson: JSON.stringify({ + tool_call_id: "call-1", + run_id: "run-1", + name: "mcp_bound", + args_json: '{ "x": 1 }', + }), + }; + await expect(consumer.handle(event, { sessionId: "s1" })).rejects.toThrow( + "MCP request unavailable", + ); + fail = false; + await consumer.handle(event, { sessionId: "s1" }); + expect(accepted).toEqual( + Array(2).fill({ + sessionId: "s1", + runId: "run-1", + toolCallId: "call-1", + requestEventIdx: "7", + deadlineMs: 930000, + }), + ); +}); diff --git a/orchestrator/src/listeners/tool-consumer.ts b/orchestrator/src/listeners/tool-consumer.ts index dabd97e20..4d30128d6 100644 --- a/orchestrator/src/listeners/tool-consumer.ts +++ b/orchestrator/src/listeners/tool-consumer.ts @@ -6,20 +6,25 @@ import { task as taskTable, taskSession as taskSessionTable } from "../db/schema import type { CuratedEvent } from "../control-plane/session-events.ts"; import { log as rootLog } from "../log.ts"; -import { makePendingToolCallStore, type PendingToolCallStore } from "../tools/pending-tool-calls.ts"; +import { + makePendingToolCallStore, + type PendingToolCallStore, +} from "../tools/pending-tool-calls.ts"; import { tools as productionTools, type ToolRegistry } from "../tools/registry.ts"; import type { ToolExecInput } from "../tools/exec.ts"; +import { MCP_LIMITS } from "../mcp/limits.ts"; +import { mcpRecovery } from "../mcp/recovery.ts"; +import { startMcpRequestWorkflow } from "../workflows/mcp-exec.ts"; import { toolExecWorkflow } from "../workflows/tool-exec.ts"; import type { SessionConsumer } from "./consumer.ts"; const log = rootLog.child({ component: "tool-consumer" }); -export type ToolWorkflowStarter = ( - input: ToolExecInput, - workflowId: string, -) => Promise; +export type ToolWorkflowStarter = (input: ToolExecInput, workflowId: string) => Promise; export interface ToolConsumerDeps { + /** Persist the exact request-event reference before the listener cursor advances. */ + startMcpRequest?: (input: import("../mcp/recovery.ts").McpRequestMarker) => Promise; registry: ToolRegistry; pendingCalls: PendingToolCallStore; startWorkflow: ToolWorkflowStarter; @@ -28,51 +33,40 @@ export interface ToolConsumerDeps { * human owner (an automation). Absent = never auto-approve. */ sessionIsOwnerless?: (sessionId: string) => Promise; /** The internal completer (`tools.complete`) the auto-approve rides. */ - completeSessionTool?: ( - sessionId: string, - toolCallId: string, - result: unknown, - ) => Promise; + completeSessionTool?: (sessionId: string, toolCallId: string, result: unknown) => Promise; } interface ToolCallRequestedPayload { toolCallId: string; name: string; argsJson?: string; + runId?: string; } -export function parseToolCallRequested( - event: CuratedEvent, -): ToolCallRequestedPayload | undefined { +export function parseToolCallRequested(event: CuratedEvent): ToolCallRequestedPayload | undefined { if (event.kind !== "tool_call_requested") return undefined; try { const payload = JSON.parse(event.payloadJson) as { tool_call_id?: unknown; name?: unknown; args_json?: unknown; + run_id?: unknown; }; - if ( - typeof payload.tool_call_id !== "string" || - typeof payload.name !== "string" - ) { + if (typeof payload.tool_call_id !== "string" || typeof payload.name !== "string") { return undefined; } return { toolCallId: payload.tool_call_id, name: payload.name, - ...(typeof payload.args_json === "string" - ? { argsJson: payload.args_json } - : {}), + ...(typeof payload.run_id === "string" ? { runId: payload.run_id } : {}), + ...(typeof payload.args_json === "string" ? { argsJson: payload.args_json } : {}), }; } catch { return undefined; } } -function eventTimestamp( - payload: Record, - now: () => Date, -): Date { +function eventTimestamp(payload: Record, now: () => Date): Date { if (typeof payload.at === "string") { const parsed = new Date(payload.at); if (!Number.isNaN(parsed.getTime())) return parsed; @@ -133,6 +127,28 @@ export function makeToolConsumer(deps: ToolConsumerDeps): SessionConsumer { await bookkeepToolEvent(ctx.sessionId, event, deps); const requested = parseToolCallRequested(event); if (!requested) return; + if (requested.name.startsWith("mcp_")) { + // Fail closed without acknowledging this event. The durable listener retries it. + if (!deps.startMcpRequest || !requested.runId || requested.argsJson === undefined) + throw Error("MCP request unavailable"); + try { + await deps.startMcpRequest({ + sessionId: ctx.sessionId, + runId: requested.runId, + toolCallId: requested.toolCallId, + requestEventIdx: String(event.idx), + deadlineMs: + deps.now().getTime() + + MCP_LIMITS.approvalMs + + MCP_LIMITS.queueMs + + MCP_LIMITS.maxToolMs, + }); + } catch { + // Listener errors can reach logs. Never retain a raw argument or driver error here. + throw Error("MCP request unavailable"); + } + return; + } const tool = deps.registry.get(requested.name); // ADR 0107 headless policy: a plan proposed in an OWNERLESS session // (an automation — nobody is watching) auto-approves immediately, so @@ -170,15 +186,10 @@ export function makeToolConsumer(deps: ToolConsumerDeps): SessionConsumer { sessionId: ctx.sessionId, toolCallId: requested.toolCallId, toolName: requested.name, - ...(requested.argsJson !== undefined - ? { argsJson: requested.argsJson } - : {}), + ...(requested.argsJson !== undefined ? { argsJson: requested.argsJson } : {}), }; try { - await deps.startWorkflow( - input, - `toolexec:${ctx.sessionId}:${requested.toolCallId}`, - ); + await deps.startWorkflow(input, `toolexec:${ctx.sessionId}:${requested.toolCallId}`); } catch (err) { log.error( { @@ -198,6 +209,10 @@ export function makeToolConsumer(deps: ToolConsumerDeps): SessionConsumer { export function makeProductionToolConsumer(): SessionConsumer { const pendingCalls = makePendingToolCallStore(); return makeToolConsumer({ + startMcpRequest: async (input) => { + if (!mcpRecovery()) throw Error("MCP runtime unavailable"); + await startMcpRequestWorkflow(input); + }, registry: productionTools, pendingCalls, now: () => new Date(), diff --git a/orchestrator/src/mcp/effect-driver.ts b/orchestrator/src/mcp/effect-driver.ts index caa61d49d..e40aae3fa 100644 --- a/orchestrator/src/mcp/effect-driver.ts +++ b/orchestrator/src/mcp/effect-driver.ts @@ -21,7 +21,11 @@ import { requestPolicyContext } from "./journal-review.ts"; import { MCP_LIMITS } from "./limits.ts"; import { lockRuntimeAuthority } from "./runtime-authority.ts"; import { makeSessionWritePolicy, type RequestPolicyContext } from "./session-policy.ts"; -import type { SessionBindingResolver, ResolvedBoundTool } from "./session-resolver.ts"; +import { + resolveSnapshotTool, + type SessionBindingResolver, + type ResolvedBoundTool, +} from "./session-resolver.ts"; import type { McpSessionBindingSnapshot } from "./session-bindings.ts"; import { makeResourceWorkerClient } from "./worker-client.ts"; import { toWorkerBindingHash } from "./worker-encoding.ts"; @@ -36,6 +40,30 @@ export interface McpToolRequest { /** Exact first coordinator tool_call_requested.args_json, not parsed and serialized again. */ originalArguments: string; } +const requestDenials = { + invalid_arguments: "MCP arguments invalid or exceed limit", + tool_unavailable: "MCP tool unavailable", + publication_required: "MCP profile grants require administrator publication", + disabled: "external MCP is disabled", + session_unavailable: "MCP session unavailable", + authority_changed: "MCP execution authority changed", + principal_unavailable: "MCP principal unavailable", + write_not_allowed: "MCP write authority unavailable", + deadline_expired: "MCP request deadline expired", +} as const; +export type McpRequestDenialCode = keyof typeof requestDenials; +/** A known local rejection, before any execution effect. No raw values or underlying error. */ +export class McpRequestDenied extends Error { + constructor(readonly code: McpRequestDenialCode) { + super(requestDenials[code]); + } +} +export class McpRequestConflict extends Error { + readonly code = "existing_request"; + constructor() { + super("MCP call identity changed"); + } +} class InvalidArguments extends Error { constructor(readonly lease: Lease) { super("MCP arguments do not satisfy approved schemas"); @@ -45,10 +73,15 @@ function object(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function argumentsValue(raw: string): Record { - if (Buffer.byteLength(raw) > MCP_LIMITS.argumentBytes) throw Error("MCP arguments exceed limit"); - const value: unknown = JSON.parse(raw); - if (!object(value)) throw Error("MCP arguments must be an object"); - return value; + try { + if (Buffer.byteLength(raw) > MCP_LIMITS.argumentBytes) + throw new McpRequestDenied("invalid_arguments"); + const value: unknown = JSON.parse(raw); + if (!object(value)) throw new McpRequestDenied("invalid_arguments"); + return value; + } catch { + throw new McpRequestDenied("invalid_arguments"); + } } function publication(snapshot: McpSessionBindingSnapshot) { const value = snapshot.profilePublication; @@ -59,7 +92,7 @@ function publication(snapshot: McpSessionBindingSnapshot) { !value.actorId || value.grantsHash !== snapshot.profileGrantsHash ) - throw Error("MCP profile grants require administrator publication"); + throw new McpRequestDenied("publication_required"); } /** The authenticated worker envelope is verified by the client and broker first. */ @@ -169,7 +202,7 @@ export function makeMcpEffectDriver(deps: { policy = makeSessionWritePolicy(deps.policy); function enabled(resolved: ResolvedBoundTool, signal?: AbortSignal) { signal?.throwIfAborted(); - if (!deps.enabled?.(resolved.binding.config.zone)) throw Error("external MCP is disabled"); + if (!deps.enabled?.(resolved.binding.config.zone)) throw new McpRequestDenied("disabled"); } async function lookup(input: McpToolRequest) { const [call] = await db @@ -187,7 +220,7 @@ export function makeMcpEffectDriver(deps: { call.originalArguments !== input.originalArguments || call.requestContext?.resolvedTool.modelToolName !== input.modelToolName) ) - throw Error("MCP call identity changed"); + throw new McpRequestConflict(); return call; } async function fence(context: RequestPolicyContext, resolved: ResolvedBoundTool, raw: string) { @@ -195,7 +228,7 @@ export function makeMcpEffectDriver(deps: { const status = await (resolved.reviewedTool.classification === "write" ? policy.validateDispatchFence(context, expected) : policy.validateExecutionIdentity(context, expected)); - if (status !== "current") throw Error("MCP execution authority changed"); + if (status !== "current") throw new McpRequestDenied("authority_changed"); } async function current(call: Call, signal?: AbortSignal) { const resolved = call.requestContext?.resolvedTool; @@ -312,13 +345,15 @@ export function makeMcpEffectDriver(deps: { const existing = await lookup(input); if (existing) return existing; const parsed = argumentsValue(input.originalArguments); - const resolved = await deps.resolver.resolveBoundTool(input); - if (!resolved) throw Error("MCP tool unavailable"); - enabled(resolved); + // A transport failure must remain retryable, not become a durable missing-tool denial. const snapshot = await deps.resolver.readCurrent(input.sessionId); + const resolved = resolveSnapshotTool(snapshot, input.modelToolName); + if (!resolved) throw new McpRequestDenied("tool_unavailable"); + enabled(resolved); publication(snapshot); const currentPolicy = await policy.readForRequest(input); - if (!currentPolicy.current || !currentPolicy.ref) throw Error("MCP session unavailable"); + if (!currentPolicy.current || !currentPolicy.ref) + throw new McpRequestDenied("session_unavailable"); const context: RequestPolicyContext = { sessionId: input.sessionId, runId: input.runId, @@ -327,19 +362,24 @@ export function makeMcpEffectDriver(deps: { acceptedWriteEligibility: currentPolicy.writeEligibility, ancestorPolicyRefs: currentPolicy.ancestorPolicyRefs, }; + if ( + resolved.reviewedTool.classification === "write" && + context.acceptedWriteEligibility !== "write_eligible" + ) + throw new McpRequestDenied("write_not_allowed"); await fence(context, resolved, input.originalArguments); const [session] = await db .select() .from(taskSession) .where(eq(taskSession.sessionId, input.sessionId)); - if (!session?.integrationPrincipalId) throw Error("MCP principal unavailable"); + if (!session?.integrationPrincipalId) throw new McpRequestDenied("principal_unavailable"); const write = resolved.reviewedTool.classification === "write"; if ( write && (context.acceptedWriteEligibility !== "write_eligible" || (await journal.hasUnknown(input.sessionId, resolved.binding.connectionId))) ) - throw Error("MCP write authority unavailable"); + throw new McpRequestDenied("write_not_allowed"); const authorization = !write ? { kind: "read" as const } : session.integrationPrincipalId.startsWith("automation:") @@ -354,7 +394,7 @@ export function makeMcpEffectDriver(deps: { MCP_LIMITS.maxToolMs; const deadline = new Date(Math.min(cap, trustedDeadline?.getTime() ?? cap)); if (!Number.isFinite(deadline.getTime()) || deadline <= now) - throw Error("MCP request deadline expired"); + throw new McpRequestDenied("deadline_expired"); let call: Call; try { call = await journal.create( diff --git a/orchestrator/src/mcp/recovery-scanner.ts b/orchestrator/src/mcp/recovery-scanner.ts new file mode 100644 index 000000000..63d1e5ee6 --- /dev/null +++ b/orchestrator/src/mcp/recovery-scanner.ts @@ -0,0 +1,58 @@ +import { makeMcpJournal } from "./journal.ts"; +import { type McpRecovery, type McpExecutionMarker, mcpRecovery } from "./recovery.ts"; +import { startMcpWorkflow } from "../workflows/mcp-exec.ts"; +import { log } from "../log.ts"; + +/** Bounded database maintenance, with a keyset cursor so parked calls cannot starve later work. */ +export function makeMcpRecoveryScanner(deps: { + deploymentId: string; + journal?: ReturnType; + runtime?: () => McpRecovery | undefined; + startWorkflow?: (marker: McpExecutionMarker, workflowId: string) => Promise; + now?: () => Date; +}) { + let after: string | undefined; + let timer: ReturnType | undefined; + let running: Promise | undefined; + let stopped = true; + async function runOnce() { + const journal = deps.journal ?? makeMcpJournal(); + const now = (deps.now ?? (() => new Date()))(); + // These producers recover distinct crash states. Disabling dispatch never disables expiry. + await journal.expire(deps.deploymentId, now); + await journal.expireUnadmitted(now); + const runtime = (deps.runtime ?? mcpRecovery)(); + if (!runtime) return; + const rows = await runtime.pending(after); + for (const { workflowId, ...marker } of rows) { + await (deps.startWorkflow ?? startMcpWorkflow)(marker, workflowId); + after = marker.executionId; + } + if (rows.length === 0) after = undefined; + } + async function tick() { + try { + await runOnce(); + } catch { + log.warn("MCP recovery scan unavailable"); + } + if (!stopped) + timer = setTimeout(() => { + running = tick(); + }, 1_000); + } + return { + runOnce, + async start() { + if (!stopped) return; + stopped = false; + running = tick(); + await running; + }, + async stop() { + stopped = true; + clearTimeout(timer); + await running; + }, + }; +} diff --git a/orchestrator/src/mcp/recovery.ts b/orchestrator/src/mcp/recovery.ts new file mode 100644 index 000000000..650a04bcd --- /dev/null +++ b/orchestrator/src/mcp/recovery.ts @@ -0,0 +1,275 @@ +import { Code, ConnectError } from "@connectrpc/connect"; +import { log } from "../log.ts"; +import { and, asc, eq, gt, lt, sql } from "drizzle-orm"; +import { sessions } from "../control-plane/client.ts"; +import { getDb } from "../db/client.ts"; +import { mcpExecution } from "../db/schema.ts"; +import type { ToolCallCompleter } from "../tools/complete.ts"; +import { + McpRequestDenied, + type McpRequestDenialCode, + type makeMcpEffectDriver, + type McpToolRequest, +} from "./effect-driver.ts"; +import type { CuratedEvent } from "../control-plane/session-events.ts"; +import { makeMcpJournal } from "./journal.ts"; +import { MCP_LIMITS } from "./limits.ts"; + +/** Only references cross the durable workflow boundary. */ +export interface McpExecutionMarker { + executionId: string; + sessionId: string; + runId: string; + toolCallId: string; +} +/** Durable pointer to the first authenticated coordinator request, not a guest projection. */ +export interface McpRequestMarker { + sessionId: string; + runId: string; + toolCallId: string; + requestEventIdx: string; + /** Trusted consumer clock plus the fixed maximum preacceptance budget. Never renewed. */ + deadlineMs: number; +} +export type McpRequestDecision = + | { kind: "accepted"; execution: McpExecutionMarker } + | { kind: "denied"; code: McpRequestDenialCode }; + +function marker(call: typeof mcpExecution.$inferSelect): McpExecutionMarker { + return { + executionId: call.id, + sessionId: call.sessionId, + runId: call.runId, + toolCallId: call.toolCallId, + }; +} +const active = new Set(["waiting_approval", "authorized", "dispatching"]); +// These exact CompleteToolCall dispositions prove that delivery can no longer occur. +// A host loss, generic precondition failure or transport outage does not prove deletion. +function targetGone(error: unknown): boolean { + return ( + error instanceof ConnectError && + (error.code === Code.NotFound || + (error.code === Code.FailedPrecondition && + error.metadata.get("engram-error-slug") === "snapshot_invalidated")) + ); +} +type CompletionDisposition = "enqueued" | "target_gone"; + +/** Plain runtime operations: effects and completion must never run in a DBOS step. */ +export function makeMcpRecovery(deps: { + db?: ReturnType; + driver: ReturnType; + deploymentId: string; + completer?: ToolCallCompleter; + now(): Date; + readRequestEvent?: (sessionId: string, index: bigint) => Promise; +}) { + const db = deps.db ?? getDb(); + const journal = makeMcpJournal(db); + async function existing(ref: Pick) { + return ( + await db + .select() + .from(mcpExecution) + .where( + and( + eq(mcpExecution.sessionId, ref.sessionId), + eq(mcpExecution.toolCallId, ref.toolCallId), + ), + ) + )[0]; + } + return { + /** The durable step records only this decision. It never runs a provider operation. */ + async decideRequest(ref: McpRequestMarker): Promise { + try { + const first = await existing(ref); + if (first) return { kind: "accepted", execution: marker(first) }; + if (!Number.isSafeInteger(ref.deadlineMs) || ref.deadlineMs <= deps.now().getTime()) + return { kind: "denied", code: "deadline_expired" }; + if (!/^[1-9][0-9]{0,18}$/.test(ref.requestEventIdx)) throw Error("invalid event reference"); + const index = BigInt(ref.requestEventIdx); + if (index > 9223372036854775807n) throw Error("invalid event reference"); + const event = deps.readRequestEvent + ? await deps.readRequestEvent(ref.sessionId, index) + : ( + await sessions.listSessionEvents({ + sessionId: ref.sessionId, + afterIdx: index - 1n, + limit: 1n, + }) + ).events[0]; + if (!event || event.idx !== index || event.kind !== "tool_call_requested") + throw Error("request reference unavailable"); + const payload: unknown = JSON.parse(event.payloadJson); + if ( + !payload || + typeof payload !== "object" || + !("tool_call_id" in payload) || + payload.tool_call_id !== ref.toolCallId || + !("run_id" in payload) || + payload.run_id !== ref.runId || + !("name" in payload) || + typeof payload.name !== "string" || + !payload.name.startsWith("mcp_") || + !("args_json" in payload) || + typeof payload.args_json !== "string" + ) + throw Error("request reference unavailable"); + try { + const call = await deps.driver.acceptToolRequest( + { + sessionId: ref.sessionId, + runId: ref.runId, + toolCallId: ref.toolCallId, + modelToolName: payload.name, + originalArguments: payload.args_json, + }, + new Date(ref.deadlineMs), + ); + return { kind: "accepted", execution: marker(call) }; + } catch (error) { + // Acceptance may have committed before a later check failed. That original row owns completion. + const winner = await existing(ref); + if (winner) return { kind: "accepted", execution: marker(winner) }; + if (error instanceof McpRequestDenied) return { kind: "denied", code: error.code }; + throw error; + } + } catch { + throw Error("MCP request acceptance unavailable"); + } + }, + async deliverDenial(ref: McpRequestMarker, code: McpRequestDenialCode): Promise { + let disposition: CompletionDisposition = "enqueued"; + try { + await (deps.completer ?? sessions).completeToolCall({ + sessionId: ref.sessionId, + toolCallId: ref.toolCallId, + resultJson: JSON.stringify({ + source: "runtime", + denial: code, + delivery: "available", + output: { + isError: true, + content: [{ type: "text", text: "External tool request denied." }], + }, + }), + }); + } catch (error) { + if (!targetGone(error)) throw Error("MCP denial completion unavailable"); + disposition = "target_gone"; + } + log.info( + { sessionId: ref.sessionId, toolCallId: ref.toolCallId, denial: code, disposition }, + "MCP negative completion settled", + ); + }, + async accept(input: McpToolRequest): Promise { + return marker(await deps.driver.acceptToolRequest(input)); + }, + async expire() { + await journal.expire(deps.deploymentId, deps.now()); + await journal.expireUnadmitted(deps.now()); + }, + async pending(after?: string) { + const rows = await db + .select({ + id: mcpExecution.id, + sessionId: mcpExecution.sessionId, + runId: mcpExecution.runId, + toolCallId: mcpExecution.toolCallId, + version: mcpExecution.version, + state: mcpExecution.state, + }) + .from(mcpExecution) + .where( + and( + lt(mcpExecution.completionEnqueuedVersion, mcpExecution.version), + after ? gt(mcpExecution.id, after) : undefined, + ), + ) + .orderBy(asc(mcpExecution.id)) + .limit(MCP_LIMITS.queueSize); + return rows.map(({ id, state, version, ...refs }) => ({ + ...refs, + executionId: id, + // One live driver per execution; a later committed outcome gets its own delivery marker. + workflowId: active.has(state) ? `mcp:${id}` : `mcp:${id}:result:${version}`, + })); + }, + async runOnce(ref: McpExecutionMarker, signal?: AbortSignal): Promise<"pending" | "done"> { + let call = await journal.get(ref.executionId); + if (!call) return "done"; // Existing session retention deletes its journal too. + if ( + call.sessionId !== ref.sessionId || + call.runId !== ref.runId || + call.toolCallId !== ref.toolCallId + ) + throw Error("MCP execution reference changed"); + if (call.state === "waiting_approval" || call.state === "authorized") + call = await deps.driver.driveOnce(call.id, signal); + if (active.has(call.state)) return "pending"; + if (call.completionEnqueuedVersion >= call.version) return "done"; + // A scanner can prove interruption, not an upstream outcome. Preserve that distinction. + const result = + call.result ?? + (call.state === "outcome_unknown" || call.state === "canceled" + ? { + source: "runtime", + outcome: call.state, + delivery: "unavailable", + reason: "execution_interrupted", + } + : undefined); + if (!result) throw Error("MCP committed result unavailable"); + let disposition: CompletionDisposition = "enqueued"; + try { + await (deps.completer ?? sessions).completeToolCall({ + sessionId: call.sessionId, + toolCallId: call.toolCallId, + resultJson: JSON.stringify({ ...result, resultUnavailable: call.resultUnavailable }), + }); + } catch (error) { + if (!targetGone(error)) throw Error("MCP completion unavailable"); + disposition = "target_gone"; + } + // This legacy field records delivery settlement: enqueue ACK OR authoritative target loss. + // It proves neither upstream outcome nor guest ACK. A newer result remains pending. + const settled = await db + .update(mcpExecution) + .set({ completionEnqueuedVersion: call.version }) + .where( + and( + eq(mcpExecution.id, call.id), + lt(mcpExecution.completionEnqueuedVersion, call.version), + sql`${mcpExecution.version} >= ${call.version}`, + ), + ) + .returning({ id: mcpExecution.id }); + if (settled.length) + log.info( + { + executionId: call.id, + sessionId: call.sessionId, + toolCallId: call.toolCallId, + version: call.version, + disposition, + }, + "MCP completion settled", + ); + return "done"; + }, + }; +} +export type McpRecovery = ReturnType; + +// Task 6 installs trusted operator/host composition before DBOS launch. Absence stays closed. +let runtime: McpRecovery | undefined; +export function installMcpRecovery(value: McpRecovery): void { + if (runtime) throw Error("MCP runtime already installed"); + runtime = value; +} +export function mcpRecovery(): McpRecovery | undefined { + return runtime; +} diff --git a/orchestrator/src/mcp/session-resolver.ts b/orchestrator/src/mcp/session-resolver.ts index 0d3e22f86..d5fa633c7 100644 --- a/orchestrator/src/mcp/session-resolver.ts +++ b/orchestrator/src/mcp/session-resolver.ts @@ -60,6 +60,26 @@ export interface ResolvedBoundTool { ancestorAuthorityRefs: McpAncestorAuthorityRef[]; execution: "sync" | "deferred"; } +/** Pure exact-name lookup after the caller has verified this snapshot's live authority. */ +export function resolveSnapshotTool( + snapshot: McpSessionBindingSnapshot, + modelToolName: string, +): ResolvedBoundTool | undefined { + for (const binding of snapshot.immutableBindings) { + const bound = binding.tools.find((tool) => tool.name === modelToolName); + if (bound) + return structuredClone({ + bindingHash: snapshot.bindingHash, + profilePublication: snapshot.profilePublication ?? null, + binding, + modelToolName: bound.name, + reviewedTool: bound.tool, + effectiveGrant: bound.grant, + ancestorAuthorityRefs: snapshot.ancestorAuthorityRefs, + execution: bound.tool.classification === "write" ? "deferred" : "sync", + }); + } +} /** Session-local resolution never changes the process-wide registry or accepts a destination. */ export function makeSessionBindingResolver(deps: SessionBindingResolverDeps) { async function readCurrent(sessionId: string): Promise { @@ -111,20 +131,7 @@ export function makeSessionBindingResolver(deps: SessionBindingResolverDeps) { }): Promise { try { const snapshot = await readCurrent(input.sessionId); - for (const binding of snapshot.immutableBindings) { - const bound = binding.tools.find((tool) => tool.name === input.modelToolName); - if (bound) - return structuredClone({ - bindingHash: snapshot.bindingHash, - profilePublication: snapshot.profilePublication ?? null, - binding, - modelToolName: bound.name, - reviewedTool: bound.tool, - effectiveGrant: bound.grant, - ancestorAuthorityRefs: snapshot.ancestorAuthorityRefs, - execution: bound.tool.classification === "write" ? "deferred" : "sync", - }); - } + return resolveSnapshotTool(snapshot, input.modelToolName); } catch { /* Missing authority, enforcement and malformed old snapshots all deny. */ } diff --git a/orchestrator/src/sweep/__tests__/sweeper.test.ts b/orchestrator/src/sweep/__tests__/sweeper.test.ts index 7179a1dbf..cfda7e4a0 100644 --- a/orchestrator/src/sweep/__tests__/sweeper.test.ts +++ b/orchestrator/src/sweep/__tests__/sweeper.test.ts @@ -86,10 +86,8 @@ async function fixture( const status = makeInMemoryDbosStatusStore(rows, now, { isVersionLive: async (applicationVersion, graceMs) => (await heartbeats.liveVersions(graceMs)).includes(applicationVersion), - isPodLive: async (podName, graceMs) => - (await heartbeats.livePods(graceMs)).includes(podName), - recordSweep: (workflowUuid, workflowName) => - ledger.recordSweep(workflowUuid, workflowName), + isPodLive: async (podName, graceMs) => (await heartbeats.livePods(graceMs)).includes(podName), + recordSweep: (workflowUuid, workflowName) => ledger.recordSweep(workflowUuid, workflowName), }); const cancelled: string[] = []; const deps: SweepTickDeps = { @@ -116,7 +114,9 @@ describe("runSweepTick retention", () => { const settings = makeInMemoryOrgSettingStore({ [RETENTION_KEY]: { runDetailDays: 7 } }); const store = makeInMemoryRetentionStore({ runs: [{ id: "run-old", endedAt: new Date(NOW.getTime() - 8 * DAY_MS), steps: 3 }], - workflows: [{ id: "wf-old", status: "SUCCESS", createdAt: new Date(NOW.getTime() - 8 * DAY_MS) }], + workflows: [ + { id: "wf-old", status: "SUCCESS", createdAt: new Date(NOW.getTime() - 8 * DAY_MS) }, + ], }); const f = await fixture([], { retention: { settings, store, now: () => new Date(NOW) } }); @@ -137,7 +137,9 @@ describe("runSweepTick retention", () => { test("does not prune when the lease is not held", async () => { const store = makeInMemoryRetentionStore({ - workflows: [{ id: "wf-old", status: "SUCCESS", createdAt: new Date(NOW.getTime() - 60 * DAY_MS) }], + workflows: [ + { id: "wf-old", status: "SUCCESS", createdAt: new Date(NOW.getTime() - 60 * DAY_MS) }, + ], }); const f = await fixture([], { retention: { settings: makeInMemoryOrgSettingStore(), store, now: () => new Date(NOW) }, @@ -235,6 +237,26 @@ describe("runSweepTick", () => { }); }); + test("adopts MCP delivery workflows after 48 hours or missing heartbeat history", async () => { + for (const lastSeen of [new Date(NOW.getTime() - 72 * HOUR_MS), null]) { + const f = await fixture( + [ + row("mcp-request", { name: "McpRequestWorkflow" }), + row("mcp-execution", { name: "McpExecWorkflow" }), + row("ordinary-stale"), + ], + {}, + {}, + { deadVersionLastSeen: lastSeen }, + ); + const result = await runSweepTick(f.deps); + expect(result.decisions.filter((x) => x.name.startsWith("Mcp")).map((x) => x.action)).toEqual( + ["adopted", "adopted"], + ); + expect(f.cancelled).toEqual(["ordinary-stale"]); + } + }); + test("cancels a stale adopt-policy workflow instead of adopting it", async () => { // Staleness = version abandonment: DEAD_VERSION last heartbeat 2h ago, // past ToolExecWorkflow's 1h window. The workflow's own age is irrelevant. @@ -269,9 +291,7 @@ describe("runSweepTick", () => { expect(result.decisions[0]?.action).toBe("adopted"); expect(f.cancelled).toEqual([]); - expect( - f.status.inspect("wf-long-lived-thread")?.applicationVersion, - ).toBeNull(); + expect(f.status.inspect("wf-long-lived-thread")?.applicationVersion).toBeNull(); }); test("a version with no heartbeat history is abandoned forever", async () => { @@ -319,9 +339,7 @@ describe("runSweepTick", () => { }); test("reports an unknown workflow name without changing it", async () => { - const f = await fixture([ - row("wf-unknown", { name: "DeletedWorkflowName" }), - ]); + const f = await fixture([row("wf-unknown", { name: "DeletedWorkflowName" })]); const result = await runSweepTick(f.deps); @@ -330,17 +348,17 @@ describe("runSweepTick", () => { expect(await f.deps.ledger.get("wf-unknown")).toBeNull(); }); - test.each([ - "temp_workflow-send-message-123", - "_dbos_debouncer_workflow", - ])("adopts a fresh prefix-covered workflow: %s", async (name) => { - const f = await fixture([row("wf-prefix", { name })]); + test.each(["temp_workflow-send-message-123", "_dbos_debouncer_workflow"])( + "adopts a fresh prefix-covered workflow: %s", + async (name) => { + const f = await fixture([row("wf-prefix", { name })]); - const result = await runSweepTick(f.deps); + const result = await runSweepTick(f.deps); - expect(result.decisions[0]?.action).toBe("adopted"); - expect(f.status.inspect("wf-prefix")?.applicationVersion).toBeNull(); - }); + expect(result.decisions[0]?.action).toBe("adopted"); + expect(f.status.inspect("wf-prefix")?.applicationVersion).toBeNull(); + }, + ); test("caps mutating actions and stops examining rows at the cap", async () => { const rows = [ @@ -358,10 +376,7 @@ describe("runSweepTick", () => { // scanned counts examined rows only: the row after the cap is left for // the next tick's cursor, not silently counted as covered. expect(result.scanned).toBe(2); - expect(result.decisions.map((decision) => decision.action)).toEqual([ - "alert_only", - "adopted", - ]); + expect(result.decisions.map((decision) => decision.action)).toEqual(["alert_only", "adopted"]); expect(f.status.inspect("wf-1")?.applicationVersion).toBeNull(); expect(f.status.inspect("wf-2")?.applicationVersion).toBe(DEAD_VERSION); }); @@ -428,9 +443,7 @@ describe("runSweepTick", () => { expect(result.scanned).toBe(40); expect(result.decisions).toHaveLength(40); - expect(f.status.inspect("wf-beyond-budget")?.applicationVersion).toBe( - DEAD_VERSION, - ); + expect(f.status.inspect("wf-beyond-budget")?.applicationVersion).toBe(DEAD_VERSION); expect(warnings).toEqual([ "DBOS orphan sweep scan budget exhausted; resuming from the cursor next cycle", ]); @@ -461,18 +474,12 @@ describe("runSweepTick", () => { const first = await runSweepTick(f.deps); expect(first.scanned).toBe(40); - expect(f.status.inspect("wf-beyond-budget")?.applicationVersion).toBe( - DEAD_VERSION, - ); + expect(f.status.inspect("wf-beyond-budget")?.applicationVersion).toBe(DEAD_VERSION); expect(scanCursor.value?.workflowUuid).toBe("wf-unknown-39"); const second = await runSweepTick(f.deps); - expect( - second.decisions.map((decision) => decision.action), - ).toEqual(["adopted"]); - expect( - f.status.inspect("wf-beyond-budget")?.applicationVersion, - ).toBeNull(); + expect(second.decisions.map((decision) => decision.action)).toEqual(["adopted"]); + expect(f.status.inspect("wf-beyond-budget")?.applicationVersion).toBeNull(); // The pass reached the end of the backlog, so the next tick restarts // from the oldest row. expect(scanCursor.value).toBeUndefined(); @@ -516,14 +523,10 @@ describe("runSweepTick", () => { const sweeper = new Sweeper(f.deps); await sweeper.runOnce(); - expect(f.status.inspect("wf-beyond-budget")?.applicationVersion).toBe( - DEAD_VERSION, - ); + expect(f.status.inspect("wf-beyond-budget")?.applicationVersion).toBe(DEAD_VERSION); await sweeper.runOnce(); - expect( - f.status.inspect("wf-beyond-budget")?.applicationVersion, - ).toBeNull(); + expect(f.status.inspect("wf-beyond-budget")?.applicationVersion).toBeNull(); }); test("a persistently failing stale cancel retries every tick until it lands", async () => { @@ -632,19 +635,13 @@ describe("runSweepTick", () => { () => new Date(NOW), { isVersionLive: async (applicationVersion, graceMs) => - (await f.heartbeats.liveVersions(graceMs)).includes( - applicationVersion, - ), + (await f.heartbeats.liveVersions(graceMs)).includes(applicationVersion), }, ); f.deps.status = { ...underlying, async listNonTerminalOnVersionsNotIn(liveVersions, limit, after) { - const listed = await underlying.listNonTerminalOnVersionsNotIn( - liveVersions, - limit, - after, - ); + const listed = await underlying.listNonTerminalOnVersionsNotIn(liveVersions, limit, after); await f.heartbeats.beat(DEAD_VERSION, "pod-returned"); return listed; }, @@ -856,7 +853,11 @@ describe("runSweepTick — stranded executors (per-pod recovery)", () => { // owner is a pod: "pod-current" beats now, "pod-dead" last beat 30 min // ago (the fixture's dead version), "local" (the SDK default id every // pod once shared) never beats at all. - function stranded(workflowUuid: string, executorId: string, overrides: Partial = {}) { + function stranded( + workflowUuid: string, + executorId: string, + overrides: Partial = {}, + ) { return row(workflowUuid, { applicationVersion: CURRENT_VERSION, name: "AutomationRunWorkflow", @@ -879,7 +880,7 @@ describe("runSweepTick — stranded executors (per-pod recovery)", () => { expect((await f.deps.ledger.get("wf-stranded"))?.sweepCount).toBe(1); }); - test("the legacy executor id \"local\" is a pod that never beats: its work is re-enqueued once", async () => { + test('the legacy executor id "local" is a pod that never beats: its work is re-enqueued once', async () => { const f = await fixture([stranded("wf-local", "local")]); const result = await runSweepTick(f.deps); expect(result.decisions.map((d) => d.action)).toEqual(["requeued"]); @@ -967,6 +968,8 @@ describe("sweep policy exhaustiveness", () => { assertSweepPoliciesExhaustive([ "ToolExecWorkflow", "AutomationRunWorkflow", + "McpRequestWorkflow", + "McpExecWorkflow", ]), ).not.toThrow(); }); @@ -1020,9 +1023,7 @@ describe("VersionHeartbeat", () => { await heartbeat.start(); - expect(beats).toEqual([ - { appVersion: CURRENT_VERSION, podName: "pod-a" }, - ]); + expect(beats).toEqual([{ appVersion: CURRENT_VERSION, podName: "pod-a" }]); expect(intervalScheduled).toBe(true); await heartbeat.stop(); }); diff --git a/orchestrator/src/sweep/policy.ts b/orchestrator/src/sweep/policy.ts index 70cf1bb9b..e0ffa1144 100644 --- a/orchestrator/src/sweep/policy.ts +++ b/orchestrator/src/sweep/policy.ts @@ -8,10 +8,13 @@ import { getAllRegisteredFunctions } from "../../node_modules/@dbos-inc/dbos-sdk */ export interface SweepPolicy { mode: "adopt"; - staleAfterHours: number; + /** Null preserves durable delivery obligations across an arbitrarily long outage. */ + staleAfterHours: number | null; } export const SWEEP_POLICIES: Record = { + McpRequestWorkflow: { mode: "adopt", staleAfterHours: null }, + McpExecWorkflow: { mode: "adopt", staleAfterHours: null }, ToolExecWorkflow: { mode: "adopt", staleAfterHours: 1 }, // ADR 0119: interpreter runs park on recv (waits, session phases) for as // long as a review or a thread takes, so the launch-scale 1h staleness @@ -50,18 +53,12 @@ export function registeredWorkflowNames(): string[] { .map((registration) => registration.name); } -export function assertSweepPoliciesExhaustive( - names: string[] = registeredWorkflowNames(), -): void { +export function assertSweepPoliciesExhaustive(names: string[] = registeredWorkflowNames()): void { const missing = names.filter( (name) => - !SWEEP_POLICIES[name] && - !name.startsWith("_dbos_") && - !name.startsWith("temp_workflow-"), + !SWEEP_POLICIES[name] && !name.startsWith("_dbos_") && !name.startsWith("temp_workflow-"), ); if (missing.length > 0) { - throw new Error( - `DBOS workflows missing sweep policies: ${missing.join(", ")}`, - ); + throw new Error(`DBOS workflows missing sweep policies: ${missing.join(", ")}`); } } diff --git a/orchestrator/src/sweep/sweeper.ts b/orchestrator/src/sweep/sweeper.ts index e74bac09c..173867079 100644 --- a/orchestrator/src/sweep/sweeper.ts +++ b/orchestrator/src/sweep/sweeper.ts @@ -26,7 +26,7 @@ export const ALERT_ONLY_STALE_AFTER_HOURS = 48; // dbos_version_heartbeats accrues one row per (version, pod) across deploys // and nothing else deletes them. Rows older than the grace window are already // dead for liveness, so pruning at a comfortable multiple only bounds growth. -// INVARIANT: retention must exceed every policy's staleAfterHours — the +// INVARIANT: retention must exceed every finite policy's staleAfterHours — the // heartbeat history is also the staleness clock (a version with no surviving // rows reads as abandoned forever), so pruning too aggressively would make a // freshly-stranded workflow look ancient. @@ -115,11 +115,7 @@ const MUTATING_ACTIONS = new Set([ "cancelled_stale", ]); -const ALERT_ACTIONS = new Set([ - "alert_only", - "cancelled_stale", - "error", -]); +const ALERT_ACTIONS = new Set(["alert_only", "cancelled_stale", "error"]); function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); @@ -143,19 +139,14 @@ function logCycle(log: Logger, result: SweepTickResult): void { ); } -export async function runSweepTick( - deps: SweepTickDeps, -): Promise { +export async function runSweepTick(deps: SweepTickDeps): Promise { const result: SweepTickResult = { leaseHeld: false, liveVersions: [], scanned: 0, decisions: [], }; - const leaseHeld = await deps.lease.tryAcquire( - deps.owner, - 2 * deps.config.sweepIntervalMs, - ); + const leaseHeld = await deps.lease.tryAcquire(deps.owner, 2 * deps.config.sweepIntervalMs); if (!leaseHeld) { logCycle(deps.log, result); return result; @@ -163,9 +154,7 @@ export async function runSweepTick( result.leaseHeld = true; try { - const liveVersions = await deps.heartbeats.liveVersions( - deps.config.graceMs, - ); + const liveVersions = await deps.heartbeats.liveVersions(deps.config.graceMs); result.liveVersions = liveVersions; const selfVersion = deps.appVersion(); if (!liveVersions.includes(selfVersion)) { @@ -188,16 +177,10 @@ export async function runSweepTick( Math.max(HEARTBEAT_RETENTION_MS, 2 * deps.config.graceMs), ); if (pruned > 0) { - deps.log.info( - { component: "dbos-sweep", pruned }, - "pruned stale version-heartbeat rows", - ); + deps.log.info({ component: "dbos-sweep", pruned }, "pruned stale version-heartbeat rows"); } } catch (error) { - deps.log.warn( - { error }, - "failed to prune stale version-heartbeat rows", - ); + deps.log.warn({ error }, "failed to prune stale version-heartbeat rows"); } // Staleness is measured from ABANDONMENT — when the workflow's owning @@ -242,7 +225,11 @@ export async function runSweepTick( }; } else { const requeued = await deps.status.requeueStrandedPendingRecording( - { workflowUuid: row.workflowUuid, executorId: row.executorId, workflowName: row.name }, + { + workflowUuid: row.workflowUuid, + executorId: row.executorId, + workflowName: row.name, + }, deps.config.podGraceMs, ); decision = requeued.flipped @@ -271,10 +258,7 @@ export async function runSweepTick( if (MUTATING_ACTIONS.has(decision.action)) actions++; } } catch (error) { - deps.log.warn( - { error }, - "DBOS stranded-executor scan failed; version scan continues", - ); + deps.log.warn({ error }, "DBOS stranded-executor scan failed; version scan continues"); } const abandonedMsByVersion = await deps.heartbeats.abandonedMsByVersion(); @@ -285,21 +269,11 @@ export async function runSweepTick( let after = cursor?.value; let exhausted = false; - scan: while ( - actions < deps.config.batchCap && - result.scanned < scanBudget - ) { - const rows = await deps.status.listNonTerminalOnVersionsNotIn( - liveVersions, - pageSize, - after, - ); + scan: while (actions < deps.config.batchCap && result.scanned < scanBudget) { + const rows = await deps.status.listNonTerminalOnVersionsNotIn(liveVersions, pageSize, after); for (const row of rows) { - if ( - actions >= deps.config.batchCap || - result.scanned >= scanBudget - ) { + if (actions >= deps.config.batchCap || result.scanned >= scanBudget) { break scan; } result.scanned++; @@ -325,18 +299,14 @@ export async function runSweepTick( // ERROR and is never re-swept; the rare silent zombie is an // operator call via the ledger's `suppressed` flag, with // sweep_count kept as the evidence trail. - const abandonedMs = abandonedMsByVersion.get( - row.applicationVersion!, - ); + const abandonedMs = abandonedMsByVersion.get(row.applicationVersion!); const abandonedHours = abandonedMs === undefined ? Number.POSITIVE_INFINITY : abandonedMs / (60 * 60 * 1_000); const staleAfterHours = - policy.mode === "alert-only" - ? ALERT_ONLY_STALE_AFTER_HOURS - : policy.staleAfterHours; - if (abandonedHours > staleAfterHours) { + policy.mode === "alert-only" ? ALERT_ONLY_STALE_AFTER_HOURS : policy.staleAfterHours; + if (staleAfterHours !== null && abandonedHours > staleAfterHours) { await deps.ledger.recordSweep(row.workflowUuid, row.name); await deps.cancelWorkflow(row.workflowUuid); decision = { @@ -345,8 +315,7 @@ export async function runSweepTick( action: "cancelled_stale", ...(policy.mode === "alert-only" ? { - reason: - "unregistered workflow name past the stale window", + reason: "unregistered workflow name past the stale window", } : {}), }; @@ -379,15 +348,14 @@ export async function runSweepTick( reason: "owner became live or row changed", }; } else if (row.status === "ENQUEUED") { - const cleared = - await deps.status.clearVersionOnEnqueuedRecording( - { - workflowUuid: row.workflowUuid, - expectedVersion: row.applicationVersion!, - workflowName: row.name, - }, - deps.config.graceMs, - ); + const cleared = await deps.status.clearVersionOnEnqueuedRecording( + { + workflowUuid: row.workflowUuid, + expectedVersion: row.applicationVersion!, + workflowName: row.name, + }, + deps.config.graceMs, + ); decision = cleared.flipped ? { workflowUuid: row.workflowUuid, @@ -429,11 +397,7 @@ export async function runSweepTick( } } - if ( - !exhausted && - actions < deps.config.batchCap && - result.scanned >= scanBudget - ) { + if (!exhausted && actions < deps.config.batchCap && result.scanned >= scanBudget) { deps.log.warn( { component: "dbos-sweep", @@ -452,20 +416,13 @@ export async function runSweepTick( ALERT_ACTIONS.has(decision.action), ).length; } catch (error) { - deps.log.warn( - { error }, - "DBOS sweep decision alerting failed", - ); + deps.log.warn({ error }, "DBOS sweep decision alerting failed"); } try { - result.failureScan = - await deps.alerter.scanTerminalFailures(); + result.failureScan = await deps.alerter.scanTerminalFailures(); } catch (error) { - deps.log.warn( - { error }, - "DBOS terminal failure scan failed", - ); + deps.log.warn({ error }, "DBOS terminal failure scan failed"); } } @@ -490,10 +447,7 @@ export async function runSweepTick( } export interface SweeperDeps extends SweepTickDeps { - setInterval?: ( - fn: () => void, - ms: number, - ) => ReturnType; + setInterval?: (fn: () => void, ms: number) => ReturnType; clearInterval?: (timer: ReturnType) => void; } @@ -550,10 +504,7 @@ export interface VersionHeartbeatDeps { heartbeats: HeartbeatStore; intervalMs: number; log: Logger; - setInterval?: ( - fn: () => void, - ms: number, - ) => ReturnType; + setInterval?: (fn: () => void, ms: number) => ReturnType; clearInterval?: (timer: ReturnType) => void; } diff --git a/orchestrator/src/workflows/mcp-exec.ts b/orchestrator/src/workflows/mcp-exec.ts new file mode 100644 index 000000000..6b3c7eff2 --- /dev/null +++ b/orchestrator/src/workflows/mcp-exec.ts @@ -0,0 +1,73 @@ +import { DBOS } from "@dbos-inc/dbos-sdk"; +import { + mcpRecovery, + type McpExecutionMarker, + type McpRequestMarker, + type McpRequestDecision, +} from "../mcp/recovery.ts"; + +async function mcpExecWorkflowEntry(marker: McpExecutionMarker): Promise { + for (;;) { + try { + const runtime = mcpRecovery(); + // Plain calls deliberately run outside DBOS steps: only the journal authorizes effects. + if (runtime && (await runtime.runOnce(marker)) === "done") return; + } catch { + // All errors stay transient and private. Original journal deadlines are never renewed. + } + await DBOS.sleep(1_000); + } +} +export const mcpExecWorkflow = DBOS.registerWorkflow(mcpExecWorkflowEntry, { + name: "McpExecWorkflow", +}); +export async function startMcpWorkflow( + marker: McpExecutionMarker, + workflowId = `mcp:${marker.executionId}`, +): Promise { + await DBOS.startWorkflow(mcpExecWorkflow, { workflowID: workflowId })(marker); +} + +/** The first decision is durable before either delivery or child execution starts. */ +async function mcpRequestWorkflowEntry(ref: McpRequestMarker): Promise { + let decision: McpRequestDecision; + for (;;) { + try { + decision = await DBOS.runStep( + async () => { + const runtime = mcpRecovery(); + if (!runtime) throw Error("MCP request acceptance unavailable"); + return runtime.decideRequest(ref); + }, + { name: "mcp-request-decision", retriesAllowed: false }, + ); + break; + } catch { + await DBOS.sleep(1_000); + } + } + if (decision.kind === "accepted") { + await startMcpWorkflow(decision.execution); + return; + } + for (;;) { + try { + const runtime = mcpRecovery(); + if (runtime) { + await runtime.deliverDenial(ref, decision.code); + return; + } + } catch { + /* Retry only the fixed negative completion, never acceptance. */ + } + await DBOS.sleep(1_000); + } +} +export const mcpRequestWorkflow = DBOS.registerWorkflow(mcpRequestWorkflowEntry, { + name: "McpRequestWorkflow", +}); +export async function startMcpRequestWorkflow(ref: McpRequestMarker): Promise { + await DBOS.startWorkflow(mcpRequestWorkflow, { + workflowID: `mcp-request:${ref.sessionId}:${ref.toolCallId}`, + })(ref); +} From 71498061cbeafb88d9ed01784793cade2388c3d7 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 06:34:55 -0500 Subject: [PATCH 027/279] Add MCP lifecycle reconciliation and reviewed rebind --- .../proto/engram/app/v1/integration.proto | 59 + .../drizzle/0112_mcp_lifecycle_receipts.sql | 14 + .../drizzle/0113_mcp_lifecycle_active.sql | 1 + orchestrator/drizzle/meta/0112_snapshot.json | 8625 ++++++++++++++++ orchestrator/drizzle/meta/0113_snapshot.json | 8641 +++++++++++++++++ orchestrator/drizzle/meta/_journal.json | 14 + .../src/__tests__/mcp-actions-rpc.test.ts | 108 + .../src/__tests__/mcp-active-watch.test.ts | 47 + .../mcp-authority-notification.test.ts | 63 + .../src/__tests__/mcp-dbos-restart.test.ts | 9 +- .../src/__tests__/mcp-effect-driver.test.ts | 157 +- .../__tests__/mcp-lifecycle-actions.test.ts | 206 + .../__tests__/mcp-lifecycle-cursor.test.ts | 70 + .../mcp-lifecycle-interceptor.test.ts | 65 + .../mcp-lifecycle-invalidation.test.ts | 21 + .../mcp-lifecycle-reconciler.test.ts | 184 + .../src/__tests__/mcp-lifecycle-rpc.test.ts | 71 + .../__tests__/mcp-preflight-rejection.test.ts | 8 +- .../src/__tests__/mcp-rebind-deny.test.ts | 90 + .../mcp-rebind-dispatch-race.test.ts | 417 + orchestrator/src/__tests__/mcp-rebind.test.ts | 863 ++ .../src/__tests__/mcp-review-store.test.ts | 9 +- orchestrator/src/__tests__/mcp-rpc.test.ts | 2 + .../__tests__/mcp-runtime-authority.test.ts | 2 +- orchestrator/src/__tests__/mcp-store.test.ts | 9 +- .../src/__tests__/mcp-unknown-drain.test.ts | 64 + orchestrator/src/control-plane/transport.ts | 14 + .../src/db/integration-connections.ts | 62 +- orchestrator/src/db/profiles.ts | 6 +- orchestrator/src/db/schema.ts | 28 + .../src/gen/engram/app/v1/integration_pb.ts | 361 +- orchestrator/src/mcp/active-watch.ts | 54 + orchestrator/src/mcp/authority-error.ts | 2 + .../src/mcp/authority-notification.ts | 17 + orchestrator/src/mcp/binding-store.ts | 13 +- orchestrator/src/mcp/contracts.ts | 162 +- orchestrator/src/mcp/credential-store.ts | 11 +- orchestrator/src/mcp/effect-driver.ts | 284 +- orchestrator/src/mcp/journal-review.ts | 53 +- orchestrator/src/mcp/journal.ts | 185 +- orchestrator/src/mcp/lifecycle-events.ts | 60 + orchestrator/src/mcp/lifecycle-interceptor.ts | 25 + .../src/mcp/lifecycle-invalidation.ts | 37 + orchestrator/src/mcp/lifecycle-reconciler.ts | 137 + orchestrator/src/mcp/rebind-deny.ts | 96 + orchestrator/src/mcp/rebind.ts | 252 + orchestrator/src/mcp/recovery-scanner.ts | 20 + orchestrator/src/mcp/recovery.ts | 51 + orchestrator/src/mcp/repository.ts | 12 +- orchestrator/src/mcp/review-state.ts | 29 +- orchestrator/src/mcp/runtime-authority.ts | 11 +- orchestrator/src/mcp/session-bindings.ts | 2 +- orchestrator/src/mcp/session-resolver.ts | 19 +- ...gration-IntegrationService_connectquery.ts | 5 + web/src/gen/engram/app/v1/integration_pb.ts | 361 +- 55 files changed, 21877 insertions(+), 311 deletions(-) create mode 100644 orchestrator/drizzle/0112_mcp_lifecycle_receipts.sql create mode 100644 orchestrator/drizzle/0113_mcp_lifecycle_active.sql create mode 100644 orchestrator/drizzle/meta/0112_snapshot.json create mode 100644 orchestrator/drizzle/meta/0113_snapshot.json create mode 100644 orchestrator/src/__tests__/mcp-actions-rpc.test.ts create mode 100644 orchestrator/src/__tests__/mcp-active-watch.test.ts create mode 100644 orchestrator/src/__tests__/mcp-authority-notification.test.ts create mode 100644 orchestrator/src/__tests__/mcp-lifecycle-actions.test.ts create mode 100644 orchestrator/src/__tests__/mcp-lifecycle-cursor.test.ts create mode 100644 orchestrator/src/__tests__/mcp-lifecycle-interceptor.test.ts create mode 100644 orchestrator/src/__tests__/mcp-lifecycle-invalidation.test.ts create mode 100644 orchestrator/src/__tests__/mcp-lifecycle-reconciler.test.ts create mode 100644 orchestrator/src/__tests__/mcp-lifecycle-rpc.test.ts create mode 100644 orchestrator/src/__tests__/mcp-rebind-deny.test.ts create mode 100644 orchestrator/src/__tests__/mcp-rebind-dispatch-race.test.ts create mode 100644 orchestrator/src/__tests__/mcp-rebind.test.ts create mode 100644 orchestrator/src/__tests__/mcp-unknown-drain.test.ts create mode 100644 orchestrator/src/mcp/active-watch.ts create mode 100644 orchestrator/src/mcp/authority-error.ts create mode 100644 orchestrator/src/mcp/authority-notification.ts create mode 100644 orchestrator/src/mcp/lifecycle-events.ts create mode 100644 orchestrator/src/mcp/lifecycle-interceptor.ts create mode 100644 orchestrator/src/mcp/lifecycle-invalidation.ts create mode 100644 orchestrator/src/mcp/lifecycle-reconciler.ts create mode 100644 orchestrator/src/mcp/rebind-deny.ts create mode 100644 orchestrator/src/mcp/rebind.ts diff --git a/crates/engram-protocol/proto/engram/app/v1/integration.proto b/crates/engram-protocol/proto/engram/app/v1/integration.proto index a85347642..f4c0b5edf 100644 --- a/crates/engram-protocol/proto/engram/app/v1/integration.proto +++ b/crates/engram-protocol/proto/engram/app/v1/integration.proto @@ -67,6 +67,7 @@ service IntegrationService { rpc DecideMcpApproval(DecideMcpApprovalRequest) returns (DecideMcpApprovalResponse); rpc CancelMcpCall(CancelMcpCallRequest) returns (CancelMcpCallResponse); rpc ResolveMcpOutcome(ResolveMcpOutcomeRequest) returns (ResolveMcpOutcomeResponse); + rpc PrepareMcpRebind(PrepareMcpRebindRequest) returns (PrepareMcpRebindResponse); rpc RebindMcp(RebindMcpRequest) returns (RebindMcpResponse); } @@ -443,20 +444,59 @@ message DecideMcpApprovalResponse { } message CancelMcpCallRequest { string execution_id = 1; + uint32 expected_journal_revision = 2; } message CancelMcpCallResponse { string state = 1; + McpCancellationReceipt receipt = 2; } message ResolveMcpOutcomeRequest { string execution_id = 1; string outcome = 2; + McpUnknownContext expected_unknown_context = 3; } message ResolveMcpOutcomeResponse { string outcome = 1; + McpResolutionReceipt receipt = 2; +} +message PrepareMcpRebindRequest { + string session_id = 1; + string expected_binding_hash = 2; +} +message PrepareMcpRebindResponse { + string binding_hash = 1; + string binding_epoch = 2; + repeated McpBoundToolSummary binding_tools = 3; + repeated string deny_hosts = 4; + string deny_revision = 5; + string deny_digest = 6; + repeated McpRebindConnection connections = 7; + McpRebindDrain drain = 8; +} +enum McpRebindDrainReason { + MCP_REBIND_DRAIN_REASON_UNSPECIFIED = 0; + MCP_REBIND_DRAIN_REASON_DISPATCH_IN_PROGRESS = 1; + MCP_REBIND_DRAIN_REASON_UNKNOWN_INTENT = 2; + MCP_REBIND_DRAIN_REASON_LEGACY_INTENT = 3; +} +message McpRebindDrain { + McpRebindDrainReason reason = 1; + string retry_after = 2; +} +message McpRebindConnection { + string connection_id = 1; + string alias = 2; + string endpoint = 3; + string zone = 4; + string credential_identity = 5; + uint32 config_revision = 6; + uint32 auth_generation = 7; + uint32 catalog_revision = 8; } message RebindMcpRequest { string session_id = 1; string expected_binding_hash = 2; + string expected_candidate_hash = 3; } message RebindMcpResponse { string binding_hash = 1; @@ -586,12 +626,31 @@ message McpExecutionReview { McpTool reviewed_tool = 15; repeated string argument_constraint_documents_json = 16; string binding_hash = 17; + McpUnknownContext expected_unknown_context = 18; + McpCancellationReceipt cancellation_receipt = 19; +} +message McpUnknownContext { + string execution_id = 1; + uint32 journal_revision = 2; + string request_digest = 3; + string binding_hash = 4; + string intent_id = 5; +} +message McpCancellationReceipt { + string execution_id = 1; + string actor_id = 2; + string cause = 3; + uint32 expected_journal_revision = 4; + uint32 journal_revision = 5; + string state = 6; + string created_at = 7; } message McpResolutionReceipt { string execution_id = 1; string actor_id = 2; string outcome = 3; string created_at = 4; + McpUnknownContext expected_context = 5; } message McpBindingReceipt { string binding_hash = 1; diff --git a/orchestrator/drizzle/0112_mcp_lifecycle_receipts.sql b/orchestrator/drizzle/0112_mcp_lifecycle_receipts.sql new file mode 100644 index 000000000..b78a4cd14 --- /dev/null +++ b/orchestrator/drizzle/0112_mcp_lifecycle_receipts.sql @@ -0,0 +1,14 @@ +CREATE TABLE "mcp_cancellation" ( + "execution_id" text PRIMARY KEY NOT NULL, + "actor_id" text, + "cause" text NOT NULL, + "expected_journal_revision" integer NOT NULL, + "journal_revision" integer NOT NULL, + "state" text NOT NULL, + "created_at" timestamp with time zone NOT NULL, + CONSTRAINT "mcp_cancellation_revision_check" CHECK ("mcp_cancellation"."expected_journal_revision" > 0 AND "mcp_cancellation"."journal_revision" = "mcp_cancellation"."expected_journal_revision" + 1), + CONSTRAINT "mcp_cancellation_state_check" CHECK ("mcp_cancellation"."state" IN ('canceled','outcome_unknown')) +); +--> statement-breakpoint +ALTER TABLE "mcp_resolution" ADD COLUMN "expected_context" jsonb;--> statement-breakpoint +ALTER TABLE "mcp_cancellation" ADD CONSTRAINT "mcp_cancellation_execution_id_mcp_execution_id_fk" FOREIGN KEY ("execution_id") REFERENCES "public"."mcp_execution"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/orchestrator/drizzle/0113_mcp_lifecycle_active.sql b/orchestrator/drizzle/0113_mcp_lifecycle_active.sql new file mode 100644 index 000000000..d0cfe9d38 --- /dev/null +++ b/orchestrator/drizzle/0113_mcp_lifecycle_active.sql @@ -0,0 +1 @@ +CREATE INDEX "mcp_execution_lifecycle_active_idx" ON "mcp_execution" USING btree ("id") WHERE "mcp_execution"."state" IN ('waiting_approval','authorized','dispatching'); \ No newline at end of file diff --git a/orchestrator/drizzle/meta/0112_snapshot.json b/orchestrator/drizzle/meta/0112_snapshot.json new file mode 100644 index 000000000..423f97999 --- /dev/null +++ b/orchestrator/drizzle/meta/0112_snapshot.json @@ -0,0 +1,8625 @@ +{ + "id": "2853fb59-ef10-4a94-8004-6075c4a3d695", + "prevId": "fc07ee98-e376-4cb1-8a53-6be93ee7a8f8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_credential": { + "name": "mcp_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()::text" + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'execute'" + }, + "schema_queue_expires_at": { + "name": "schema_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_queue_expires_at": { + "name": "execute_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_expires_at": { + "name": "execute_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "schema_digest": { + "name": "schema_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_reservation_idx": { + "name": "mcp_admission_reservation_idx", + "columns": [ + { + "expression": "reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_phase_check": { + "name": "mcp_admission_phase_check", + "value": "\"mcp_admission\".\"phase\" IN ('schema','execute')" + }, + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted','released')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_cancellation": { + "name": "mcp_cancellation", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cause": { + "name": "cause", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_journal_revision": { + "name": "expected_journal_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "journal_revision": { + "name": "journal_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_cancellation_execution_id_mcp_execution_id_fk": { + "name": "mcp_cancellation_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_cancellation", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_cancellation_revision_check": { + "name": "mcp_cancellation_revision_check", + "value": "\"mcp_cancellation\".\"expected_journal_revision\" > 0 AND \"mcp_cancellation\".\"journal_revision\" = \"mcp_cancellation\".\"expected_journal_revision\" + 1" + }, + "mcp_cancellation_state_check": { + "name": "mcp_cancellation_state_check", + "value": "\"mcp_cancellation\".\"state\" IN ('canceled','outcome_unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_arguments": { + "name": "original_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completion_enqueued_version": { + "name": "completion_enqueued_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_recovery_idx": { + "name": "mcp_execution_recovery_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_execution\".\"completion_enqueued_version\" < \"mcp_execution\".\"version\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_completion_version_check": { + "name": "mcp_execution_completion_version_check", + "value": "\"mcp_execution\".\"completion_enqueued_version\" >= 0 AND \"mcp_execution\".\"completion_enqueued_version\" <= \"mcp_execution\".\"version\"" + }, + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expected_context": { + "name": "expected_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "mcp_grant_publication": { + "name": "mcp_grant_publication", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_binding_envelope": { + "name": "mcp_binding_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/0113_snapshot.json b/orchestrator/drizzle/meta/0113_snapshot.json new file mode 100644 index 000000000..e9efde86a --- /dev/null +++ b/orchestrator/drizzle/meta/0113_snapshot.json @@ -0,0 +1,8641 @@ +{ + "id": "c7aba123-d892-4eae-abc5-fa71d8d50a4a", + "prevId": "2853fb59-ef10-4a94-8004-6075c4a3d695", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "apikey_reference_id_user_id_fk": { + "name": "apikey_reference_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "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 + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'private'" + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_owner_idx": { + "name": "artifact_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "artifact_visibility_idx": { + "name": "artifact_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_owner_user_id_user_id_fk": { + "name": "artifact_owner_user_id_user_id_fk", + "tableFrom": "artifact", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact_version": { + "name": "artifact_version", + "schema": "", + "columns": { + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coord_artifact_id": { + "name": "coord_artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "artifact_version_session_idx": { + "name": "artifact_version_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "artifact_version_artifact_id_artifact_id_fk": { + "name": "artifact_version_artifact_id_artifact_id_fk", + "tableFrom": "artifact_version", + "tableTo": "artifact", + "columnsFrom": [ + "artifact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "artifact_version_artifact_id_version_pk": { + "name": "artifact_version_artifact_id_version_pk", + "columns": [ + "artifact_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation": { + "name": "automation", + "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, + "default": "''" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "builtin_key": { + "name": "builtin_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_version": { + "name": "current_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "block_overrides": { + "name": "block_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "end_sessions_on_finish": { + "name": "end_sessions_on_finish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fire_at": { + "name": "next_fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "draft_session_id": { + "name": "draft_session_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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_due_idx": { + "name": "automation_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_draft_session_idx": { + "name": "automation_draft_session_idx", + "columns": [ + { + "expression": "draft_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_builtin_key_unique": { + "name": "automation_builtin_key_unique", + "columns": [ + { + "expression": "builtin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "builtin_key is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_concurrency_claim": { + "name": "automation_concurrency_claim", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_concurrency_claim_automation_id_automation_id_fk": { + "name": "automation_concurrency_claim_automation_id_automation_id_fk", + "tableFrom": "automation_concurrency_claim", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_concurrency_claim_automation_id_concurrency_key_pk": { + "name": "automation_concurrency_claim_automation_id_concurrency_key_pk", + "columns": [ + "automation_id", + "concurrency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_drop": { + "name": "automation_drop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dropped_at": { + "name": "dropped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_drop_automation_idx": { + "name": "automation_drop_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_drop_automation_id_automation_id_fk": { + "name": "automation_drop_automation_id_automation_id_fk", + "tableFrom": "automation_drop", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance": { + "name": "automation_instance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "automation_instance_open_key_unique": { + "name": "automation_instance_open_key_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_instance_automation_idx": { + "name": "automation_instance_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opened_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_automation_id_automation_id_fk": { + "name": "automation_instance_automation_id_automation_id_fk", + "tableFrom": "automation_instance", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_instance_handle": { + "name": "automation_instance_handle", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "written_by": { + "name": "written_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_instance_handle_instance_idx": { + "name": "automation_instance_handle_instance_idx", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_instance_handle_automation_id_automation_id_fk": { + "name": "automation_instance_handle_automation_id_automation_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_instance_handle_instance_id_automation_instance_id_fk": { + "name": "automation_instance_handle_instance_id_automation_instance_id_fk", + "tableFrom": "automation_instance_handle", + "tableTo": "automation_instance", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_instance_handle_automation_id_handle_pk": { + "name": "automation_instance_handle_automation_id_handle_pk", + "columns": [ + "automation_id", + "handle" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_run": { + "name": "automation_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "concurrency_key": { + "name": "concurrency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rendered_prompt": { + "name": "rendered_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rendered_title": { + "name": "rendered_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "entrypoint_id": { + "name": "entrypoint_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "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 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "details_pruned_at": { + "name": "details_pruned_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": { + "automation_run_retention_idx": { + "name": "automation_run_retention_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "ended_at is not null and details_pruned_at is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_occurrence_unique": { + "name": "automation_run_occurrence_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "scheduled_for is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_delivery_unique": { + "name": "automation_run_delivery_unique", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entrypoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "delivery_key is not null and scheduled_for is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_automation_created_idx": { + "name": "automation_run_automation_created_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_lease_idx": { + "name": "automation_run_lease_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_run_concurrency_idx": { + "name": "automation_run_concurrency_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "concurrency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_run_automation_id_automation_id_fk": { + "name": "automation_run_automation_id_automation_id_fk", + "tableFrom": "automation_run", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_run_task_id_task_id_fk": { + "name": "automation_run_task_id_task_id_fk", + "tableFrom": "automation_run", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session": { + "name": "automation_session", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'primary'" + }, + "keep": { + "name": "keep", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "automation_session_run_idx": { + "name": "automation_session_run_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_session_run_id_automation_run_id_fk": { + "name": "automation_session_run_id_automation_run_id_fk", + "tableFrom": "automation_session", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_session_relay": { + "name": "automation_session_relay", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "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": {}, + "foreignKeys": { + "automation_session_relay_session_id_automation_session_session_id_fk": { + "name": "automation_session_relay_session_id_automation_session_session_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_session_relay_automation_id_automation_id_fk": { + "name": "automation_session_relay_automation_id_automation_id_fk", + "tableFrom": "automation_session_relay", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_state": { + "name": "automation_state", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "writer": { + "name": "writer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "automation_state_automation_id_automation_id_fk": { + "name": "automation_state_automation_id_automation_id_fk", + "tableFrom": "automation_state", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_state_automation_id_key_pk": { + "name": "automation_state_automation_id_key_pk", + "columns": [ + "automation_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_step_run": { + "name": "automation_step_run", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inputs": { + "name": "inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "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": true, + "default": "now()" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "automation_step_run_run_id_automation_run_id_fk": { + "name": "automation_step_run_run_id_automation_run_id_fk", + "tableFrom": "automation_step_run", + "tableTo": "automation_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_step_run_run_id_block_id_attempt_pk": { + "name": "automation_step_run_run_id_block_id_attempt_pk", + "columns": [ + "run_id", + "block_id", + "attempt" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_version": { + "name": "automation_version", + "schema": "", + "columns": { + "automation_id": { + "name": "automation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "entrypoints": { + "name": "entrypoints", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "agent": { + "name": "agent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_template": { + "name": "prompt_template", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs_schema": { + "name": "inputs_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "automation_version_automation_id_automation_id_fk": { + "name": "automation_version_automation_id_automation_id_fk", + "tableFrom": "automation_version", + "tableTo": "automation", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "automation_version_automation_id_version_pk": { + "name": "automation_version_automation_id_version_pk", + "columns": [ + "automation_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector": { + "name": "connector", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connector_logo": { + "name": "connector_logo", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consumer_cursors": { + "name": "consumer_cursors", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_idx": { + "name": "last_idx", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "consumer_cursors_session_id_consumer_pk": { + "name": "consumer_cursors_session_id_consumer_pk", + "columns": [ + "session_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coordination_operation": { + "name": "coordination_operation", + "schema": "", + "columns": { + "caller_session_id": { + "name": "caller_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_task_id": { + "name": "reserved_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_session_id": { + "name": "reserved_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "coordination_operation_reserved_session_idx": { + "name": "coordination_operation_reserved_session_idx", + "columns": [ + { + "expression": "reserved_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "coordination_operation_caller_session_id_operation_idempotency_key_pk": { + "name": "coordination_operation_caller_session_id_operation_idempotency_key_pk", + "columns": [ + "caller_session_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_lease": { + "name": "dbos_sweep_lease", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_sweep_ledger": { + "name": "dbos_sweep_ledger", + "schema": "", + "columns": { + "workflow_uuid": { + "name": "workflow_uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_name": { + "name": "workflow_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sweep_count": { + "name": "sweep_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_swept_at": { + "name": "first_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_swept_at": { + "name": "last_swept_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suppressed": { + "name": "suppressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "alerted_at": { + "name": "alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_alerted_at": { + "name": "terminal_alerted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dbos_version_heartbeats": { + "name": "dbos_version_heartbeats", + "schema": "", + "columns": { + "application_version": { + "name": "application_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pod_name": { + "name": "pod_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dbos_version_heartbeats_application_version_pod_name_pk": { + "name": "dbos_version_heartbeats_application_version_pod_name_pk", + "columns": [ + "application_version", + "pod_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_code": { + "name": "device_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "device_code_device_code_idx": { + "name": "device_code_device_code_idx", + "columns": [ + { + "expression": "device_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_code_user_code_idx": { + "name": "device_code_user_code_idx", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connection": { + "name": "integration_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mcp_credential": { + "name": "mcp_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tested_at": { + "name": "tested_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": { + "integration_connection_provider_idx": { + "name": "integration_connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connection_provider_default_unique": { + "name": "integration_connection_provider_default_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connection_alias_unique": { + "name": "integration_connection_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_event": { + "name": "integration_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scope_value": { + "name": "scope_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_event_delivery_unique": { + "name": "integration_event_delivery_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_event_conn_key_idx": { + "name": "integration_event_conn_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_event_connection_id_integration_connection_id_fk": { + "name": "integration_event_connection_id_integration_connection_id_fk", + "tableFrom": "integration_event", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_oidc_key": { + "name": "integration_oidc_key", + "schema": "", + "columns": { + "kid": { + "name": "kid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_jwk": { + "name": "public_jwk", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "publish_until": { + "name": "publish_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_oidc_key_active_unique": { + "name": "integration_oidc_key_active_unique", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "state = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_admission": { + "name": "mcp_admission", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()::text" + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'execute'" + }, + "schema_queue_expires_at": { + "name": "schema_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_queue_expires_at": { + "name": "execute_queue_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execute_expires_at": { + "name": "execute_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "schema_digest": { + "name": "schema_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "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 + } + }, + "indexes": { + "mcp_admission_reservation_idx": { + "name": "mcp_admission_reservation_idx", + "columns": [ + { + "expression": "reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_deployment_idx": { + "name": "mcp_admission_deployment_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_connection_idx": { + "name": "mcp_admission_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_admission_session_idx": { + "name": "mcp_admission_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_admission_execution_id_mcp_execution_id_fk": { + "name": "mcp_admission_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_admission_deployment_id_mcp_admission_scope_id_fk": { + "name": "mcp_admission_deployment_id_mcp_admission_scope_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "mcp_admission_scope", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_admission_connection_id_integration_connection_id_fk": { + "name": "mcp_admission_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_admission", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_admission_phase_check": { + "name": "mcp_admission_phase_check", + "value": "\"mcp_admission\".\"phase\" IN ('schema','execute')" + }, + "mcp_admission_state_check": { + "name": "mcp_admission_state_check", + "value": "\"mcp_admission\".\"state\" IN ('queued','admitted','released')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_admission_scope": { + "name": "mcp_admission_scope", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_approval": { + "name": "mcp_approval", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + } + }, + "indexes": { + "mcp_approval_pending_idx": { + "name": "mcp_approval_pending_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_approval_session_idx": { + "name": "mcp_approval_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "decision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_approval_execution_id_mcp_execution_id_fk": { + "name": "mcp_approval_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_approval", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_approval_decision_check": { + "name": "mcp_approval_decision_check", + "value": "\"mcp_approval\".\"decision\" IN ('pending','approved','denied','expired')" + }, + "mcp_approval_consumed_check": { + "name": "mcp_approval_consumed_check", + "value": "\"mcp_approval\".\"consumed_at\" IS NULL OR (\"mcp_approval\".\"decision\" = 'approved' AND \"mcp_approval\".\"actor_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_cancellation": { + "name": "mcp_cancellation", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cause": { + "name": "cause", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_journal_revision": { + "name": "expected_journal_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "journal_revision": { + "name": "journal_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_cancellation_execution_id_mcp_execution_id_fk": { + "name": "mcp_cancellation_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_cancellation", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_cancellation_revision_check": { + "name": "mcp_cancellation_revision_check", + "value": "\"mcp_cancellation\".\"expected_journal_revision\" > 0 AND \"mcp_cancellation\".\"journal_revision\" = \"mcp_cancellation\".\"expected_journal_revision\" + 1" + }, + "mcp_cancellation_state_check": { + "name": "mcp_cancellation_state_check", + "value": "\"mcp_cancellation\".\"state\" IN ('canceled','outcome_unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mcp_catalog": { + "name": "mcp_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_identity": { + "name": "credential_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_catalog_connection_idx": { + "name": "mcp_catalog_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_catalog_published_revision_idx": { + "name": "mcp_catalog_published_revision_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "kind = 'published'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_catalog_connection_id_integration_connection_id_fk": { + "name": "mcp_catalog_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_catalog", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_catalog_kind_check": { + "name": "mcp_catalog_kind_check", + "value": "\"mcp_catalog\".\"kind\" IN ('draft','published') AND ((\"mcp_catalog\".\"kind\" = 'draft' AND \"mcp_catalog\".\"revision\" = 0) OR (\"mcp_catalog\".\"kind\" = 'published' AND \"mcp_catalog\".\"revision\" > 0))" + } + }, + "isRLSEnabled": false + }, + "public.mcp_execution": { + "name": "mcp_execution", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_id": { + "name": "catalog_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_hash": { + "name": "binding_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "argument_hash": { + "name": "argument_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_arguments": { + "name": "original_arguments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "upstream_name": { + "name": "upstream_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auth_generation": { + "name": "auth_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_kind": { + "name": "authorization_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "automation_grant_hash": { + "name": "automation_grant_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "boot_id": { + "name": "boot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "intent_expires_at": { + "name": "intent_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline": { + "name": "deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_unavailable": { + "name": "result_unavailable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completion_enqueued_version": { + "name": "completion_enqueued_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_execution_task_session_idx": { + "name": "mcp_execution_task_session_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_call_idx": { + "name": "mcp_execution_call_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_connection_state_idx": { + "name": "mcp_execution_connection_state_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_catalog_idx": { + "name": "mcp_execution_catalog_idx", + "columns": [ + { + "expression": "catalog_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_pending_idx": { + "name": "mcp_execution_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deadline", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_lifecycle_active_idx": { + "name": "mcp_execution_lifecycle_active_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_recovery_idx": { + "name": "mcp_execution_recovery_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_execution\".\"completion_enqueued_version\" < \"mcp_execution\".\"version\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_execution_intent_idx": { + "name": "mcp_execution_intent_idx", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_execution_connection_id_integration_connection_id_fk": { + "name": "mcp_execution_connection_id_integration_connection_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "integration_connection", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_catalog_id_mcp_catalog_id_fk": { + "name": "mcp_execution_catalog_id_mcp_catalog_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "mcp_catalog", + "columnsFrom": [ + "catalog_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk": { + "name": "mcp_execution_task_id_session_id_task_session_task_id_session_id_fk", + "tableFrom": "mcp_execution", + "tableTo": "task_session", + "columnsFrom": [ + "task_id", + "session_id" + ], + "columnsTo": [ + "task_id", + "session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_execution_completion_version_check": { + "name": "mcp_execution_completion_version_check", + "value": "\"mcp_execution\".\"completion_enqueued_version\" >= 0 AND \"mcp_execution\".\"completion_enqueued_version\" <= \"mcp_execution\".\"version\"" + }, + "mcp_execution_state_check": { + "name": "mcp_execution_state_check", + "value": "\"mcp_execution\".\"state\" IN ('waiting_approval','authorized','dispatching','succeeded','failed','canceled','outcome_unknown')" + }, + "mcp_execution_classification_check": { + "name": "mcp_execution_classification_check", + "value": "\"mcp_execution\".\"classification\" IN ('read','write')" + }, + "mcp_execution_authorization_check": { + "name": "mcp_execution_authorization_check", + "value": "(\"mcp_execution\".\"authorization_kind\" = 'read' AND \"mcp_execution\".\"classification\" = 'read' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'approval' AND \"mcp_execution\".\"automation_grant_hash\" IS NULL) OR (\"mcp_execution\".\"authorization_kind\" = 'automation' AND \"mcp_execution\".\"classification\" = 'write' AND \"mcp_execution\".\"automation_grant_hash\" IS NOT NULL)" + }, + "mcp_execution_dispatch_check": { + "name": "mcp_execution_dispatch_check", + "value": "\"mcp_execution\".\"state\" NOT IN ('dispatching','outcome_unknown','succeeded') OR (\"mcp_execution\".\"boot_id\" IS NOT NULL AND \"mcp_execution\".\"intent_id\" IS NOT NULL AND \"mcp_execution\".\"intent_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mcp_resolution": { + "name": "mcp_resolution", + "schema": "", + "columns": { + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expected_context": { + "name": "expected_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "mcp_resolution_execution_id_mcp_execution_id_fk": { + "name": "mcp_resolution_execution_id_mcp_execution_id_fk", + "tableFrom": "mcp_resolution", + "tableTo": "mcp_execution", + "columnsFrom": [ + "execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_resolution_outcome_check": { + "name": "mcp_resolution_outcome_check", + "value": "\"mcp_resolution\".\"outcome\" IN ('succeeded','failed','acknowledged')" + } + }, + "isRLSEnabled": false + }, + "public.org_setting": { + "name": "org_setting", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.papercuts": { + "name": "papercuts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "papercuts_session_tool_call_unique": { + "name": "papercuts_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_created_at_idx": { + "name": "papercuts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "papercuts_archived_at_idx": { + "name": "papercuts_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_tool_calls": { + "name": "pending_tool_calls", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "handling": { + "name": "handling", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "pending_tool_calls_session_tool_call_unique": { + "name": "pending_tool_calls_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_session_idx": { + "name": "pending_tool_calls_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_tool_calls_awaiting_idx": { + "name": "pending_tool_calls_awaiting_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"pending_tool_calls\".\"handling\" = 'session' AND \"pending_tool_calls\".\"submitted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pr_ref": { + "name": "pr_ref", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "authoring_task_id": { + "name": "authoring_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "pr_ref_repo_pr_number_unique": { + "name": "pr_ref_repo_pr_number_unique", + "columns": [ + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_authoring_task_idx": { + "name": "pr_ref_authoring_task_idx", + "columns": [ + { + "expression": "authoring_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pr_ref_session_idx": { + "name": "pr_ref_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pr_ref_authoring_task_id_task_id_fk": { + "name": "pr_ref_authoring_task_id_task_id_fk", + "tableFrom": "pr_ref", + "tableTo": "task", + "columnsFrom": [ + "authoring_task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "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, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Bot'" + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "programmatic_only": { + "name": "programmatic_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_user_tokens": { + "name": "include_user_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "env_vars": { + "name": "env_vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "mcp_grant_publication": { + "name": "mcp_grant_publication", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "network": { + "name": "network", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":\"deny\",\"allowHosts\":[],\"allowHostPatterns\":[]}'::jsonb" + }, + "secrets": { + "name": "secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repos": { + "name": "repos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "apps": { + "name": "apps", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finder_session_id": { + "name": "finder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verifier_session_id": { + "name": "verifier_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "automation_run_id": { + "name": "automation_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary_md": { + "name": "summary_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_target_idx": { + "name": "review_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_task_idx": { + "name": "review_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_one_active_per_target_idx": { + "name": "review_one_active_per_target_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"review\".\"status\" IN ('queued', 'finding', 'verifying')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finder_session_idx": { + "name": "review_finder_session_idx", + "columns": [ + { + "expression": "finder_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verifier_session_idx": { + "name": "review_verifier_session_idx", + "columns": [ + { + "expression": "verifier_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_target_id_review_target_id_fk": { + "name": "review_target_id_review_target_id_fk", + "tableFrom": "review", + "tableTo": "review_target", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "review_task_id_task_id_fk": { + "name": "review_task_id_task_id_fk", + "tableFrom": "review", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_event": { + "name": "review_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_event_review_idx": { + "name": "review_event_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_event_review_id_review_id_fk": { + "name": "review_event_review_id_review_id_fk", + "tableFrom": "review_event", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_finding": { + "name": "review_finding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_md": { + "name": "body_md", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suggested_fix": { + "name": "suggested_fix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "verdict_reason": { + "name": "verdict_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_thread_id": { + "name": "github_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_finding_session_tool_call_unique": { + "name": "review_finding_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_finding_review_idx": { + "name": "review_finding_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_finding_review_id_review_id_fk": { + "name": "review_finding_review_id_review_id_fk", + "tableFrom": "review_finding", + "tableTo": "review", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_target": { + "name": "review_target", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo": { + "name": "repo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_updated_at": { + "name": "provider_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hydration_failed_at": { + "name": "hydration_failed_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": { + "review_target_provider_id_unique": { + "name": "review_target_provider_id_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_target_coordinate_idx": { + "name": "review_target_coordinate_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_verdict": { + "name": "review_verdict", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "review_verdict_session_tool_call_unique": { + "name": "review_verdict_session_tool_call_unique", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "review_verdict_finding_unique": { + "name": "review_verdict_finding_unique", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_verdict_finding_id_review_finding_id_fk": { + "name": "review_verdict_finding_id_review_finding_id_fk", + "tableFrom": "review_verdict", + "tableTo": "review_finding", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model": { + "name": "router_model", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_slug": { + "name": "canonical_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "prompt_price": { + "name": "prompt_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completion_price": { + "name": "completion_price", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_modalities": { + "name": "output_modalities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "supported_parameters": { + "name": "supported_parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "hugging_face_id": { + "name": "hugging_face_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upstream": { + "name": "upstream", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "available": { + "name": "available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "router_model_available_idx": { + "name": "router_model_available_idx", + "columns": [ + { + "expression": "router_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "router_model_router_id_model_id_pk": { + "name": "router_model_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.router_model_policy": { + "name": "router_model_policy", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "user_enabled": { + "name": "user_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk": { + "name": "router_model_policy_router_id_model_id_router_model_router_id_model_id_fk", + "tableFrom": "router_model_policy", + "tableTo": "router_model", + "columnsFrom": [ + "router_id", + "model_id" + ], + "columnsTo": [ + "router_id", + "model_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "router_model_policy_router_id_model_id_pk": { + "name": "router_model_policy_router_id_model_id_pk", + "columns": [ + "router_id", + "model_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "router_model_policy_user_implies_enabled": { + "name": "router_model_policy_user_implies_enabled", + "value": "NOT \"router_model_policy\".\"user_enabled\" OR \"router_model_policy\".\"enabled\"" + } + }, + "isRLSEnabled": false + }, + "public.router_sync_state": { + "name": "router_sync_state", + "schema": "", + "columns": { + "router_id": { + "name": "router_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_app": { + "name": "session_app", + "schema": "", + "columns": { + "host_label": { + "name": "host_label", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'org'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_app_session_name_idx": { + "name": "session_app_session_name_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_port_idx": { + "name": "session_app_session_port_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "port", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_session_idx": { + "name": "session_app_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_app_owner_idx": { + "name": "session_app_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_app_owner_user_id_user_id_fk": { + "name": "session_app_owner_user_id_user_id_fk", + "tableFrom": "session_app", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_listeners": { + "name": "session_listeners", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dormant_at": { + "name": "dormant_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "woken_at": { + "name": "woken_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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec": { + "name": "spec", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_doc_seq": { + "name": "current_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "current_semantic_doc_seq": { + "name": "current_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "published_checkpoint_id": { + "name": "published_checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "spec_org_updated_idx": { + "name": "spec_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_owner_idx": { + "name": "spec_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_session_idx": { + "name": "spec_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_owner_user_id_user_id_fk": { + "name": "spec_owner_user_id_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_template_id_spec_template_id_fk": { + "name": "spec_template_id_spec_template_id_fk", + "tableFrom": "spec", + "tableTo": "spec_template", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spec_published_by_user_id_fk": { + "name": "spec_published_by_user_id_fk", + "tableFrom": "spec", + "tableTo": "user", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_chat_message": { + "name": "spec_chat_message", + "schema": "", + "columns": { + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_chat_message_spec_created_idx": { + "name": "spec_chat_message_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_chat_message_spec_id_spec_id_fk": { + "name": "spec_chat_message_spec_id_spec_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_chat_message_author_user_id_user_id_fk": { + "name": "spec_chat_message_author_user_id_user_id_fk", + "tableFrom": "spec_chat_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_checkpoint": { + "name": "spec_checkpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "rendered_markdown": { + "name": "rendered_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "spec_checkpoint_spec_created_idx": { + "name": "spec_checkpoint_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_checkpoint_spec_id_spec_id_fk": { + "name": "spec_checkpoint_spec_id_spec_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_checkpoint_author_user_id_user_id_fk": { + "name": "spec_checkpoint_author_user_id_user_id_fk", + "tableFrom": "spec_checkpoint", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_drafting_seed": { + "name": "spec_drafting_seed", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prompt_id": { + "name": "prompt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_drafting_seed_due_idx": { + "name": "spec_drafting_seed_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_drafting_seed\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_drafting_seed_spec_id_spec_id_fk": { + "name": "spec_drafting_seed_spec_id_spec_id_fk", + "tableFrom": "spec_drafting_seed", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "spec_drafting_seed_prompt_id_unique": { + "name": "spec_drafting_seed_prompt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "prompt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_finding": { + "name": "spec_gap_check_finding", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer_key": { + "name": "layer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requirement_id": { + "name": "requirement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_diff": { + "name": "proposed_diff", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "open_question_id": { + "name": "open_question_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disposed_by": { + "name": "disposed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposed_at": { + "name": "disposed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk": { + "name": "spec_gap_check_finding_run_id_spec_gap_check_run_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "spec_gap_check_run", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_finding_disposed_by_user_id_fk": { + "name": "spec_gap_check_finding_disposed_by_user_id_fk", + "tableFrom": "spec_gap_check_finding", + "tableTo": "user", + "columnsFrom": [ + "disposed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_gap_check_finding_run_id_finding_id_pk": { + "name": "spec_gap_check_finding_run_id_finding_id_pk", + "columns": [ + "run_id", + "finding_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_gap_check_run": { + "name": "spec_gap_check_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "stopped_at_layer_key": { + "name": "stopped_at_layer_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suppressed_count": { + "name": "suppressed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "matrix": { + "name": "matrix", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "started_by": { + "name": "started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_gap_check_run_fingerprint_idx": { + "name": "spec_gap_check_run_fingerprint_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "spec_gap_check_run_spec_created_idx": { + "name": "spec_gap_check_run_spec_created_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_gap_check_run_spec_id_spec_id_fk": { + "name": "spec_gap_check_run_spec_id_spec_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_gap_check_run_started_by_user_id_fk": { + "name": "spec_gap_check_run_started_by_user_id_fk", + "tableFrom": "spec_gap_check_run", + "tableTo": "user", + "columnsFrom": [ + "started_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_open_question": { + "name": "spec_open_question", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_by": { + "name": "opened_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_open_question_spec_section_idx": { + "name": "spec_open_question_spec_section_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "section_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_open_question_spec_id_spec_id_fk": { + "name": "spec_open_question_spec_id_spec_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_open_question_opened_by_user_id_fk": { + "name": "spec_open_question_opened_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "opened_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "spec_open_question_resolved_by_user_id_fk": { + "name": "spec_open_question_resolved_by_user_id_fk", + "tableFrom": "spec_open_question", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_participant": { + "name": "spec_participant", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_epoch": { + "name": "connection_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_participant_live_idx": { + "name": "spec_participant_live_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_participant_spec_id_spec_id_fk": { + "name": "spec_participant_spec_id_spec_id_fk", + "tableFrom": "spec_participant", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_participant_user_id_user_id_fk": { + "name": "spec_participant_user_id_user_id_fk", + "tableFrom": "spec_participant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_participant_spec_id_client_id_pk": { + "name": "spec_participant_spec_id_client_id_pk", + "columns": [ + "spec_id", + "client_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_projection": { + "name": "spec_projection", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rev": { + "name": "rev", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "doc_seq": { + "name": "doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendered": { + "name": "rendered", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "document_state": { + "name": "document_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest": { + "name": "digest", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "digest_sha256": { + "name": "digest_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staging_path": { + "name": "staging_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_source": { + "name": "requested_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_notice": { + "name": "discard_notice", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "spec_projection_session_state_idx": { + "name": "spec_projection_session_state_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rev", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_projection_spec_id_spec_id_fk": { + "name": "spec_projection_spec_id_spec_id_fk", + "tableFrom": "spec_projection", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_projection_spec_id_rev_pk": { + "name": "spec_projection_spec_id_rev_pk", + "columns": [ + "spec_id", + "rev" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_publish": { + "name": "spec_publish", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_version": { + "name": "artifact_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by": { + "name": "requested_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "acknowledged_question_count": { + "name": "acknowledged_question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "acknowledged_question_ids": { + "name": "acknowledged_question_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "gap_check_run_id": { + "name": "gap_check_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_publish_due_idx": { + "name": "spec_publish_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_publish\".\"state\" <> 'complete'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_publish_spec_id_spec_id_fk": { + "name": "spec_publish_spec_id_spec_id_fk", + "tableFrom": "spec_publish", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_publish_requested_by_user_id_fk": { + "name": "spec_publish_requested_by_user_id_fk", + "tableFrom": "spec_publish", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_section_state": { + "name": "spec_section_state", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "na_reason": { + "name": "na_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_by": { + "name": "settled_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": { + "spec_section_state_spec_id_spec_id_fk": { + "name": "spec_section_state_spec_id_spec_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_section_state_settled_by_user_id_fk": { + "name": "spec_section_state_settled_by_user_id_fk", + "tableFrom": "spec_section_state", + "tableTo": "user", + "columnsFrom": [ + "settled_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_section_state_spec_id_section_id_pk": { + "name": "spec_section_state_spec_id_section_id_pk", + "columns": [ + "spec_id", + "section_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_snapshot": { + "name": "spec_snapshot", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "state_vector": { + "name": "state_vector", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "covered_seq": { + "name": "covered_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "covered_semantic_doc_seq": { + "name": "covered_semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_snapshot_spec_id_spec_id_fk": { + "name": "spec_snapshot_spec_id_spec_id_fk", + "tableFrom": "spec_snapshot", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_template": { + "name": "spec_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "layers": { + "name": "layers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "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": { + "spec_template_org_idx": { + "name": "spec_template_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_draft": { + "name": "spec_ticket_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sync_state": { + "name": "sync_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "linear_id": { + "name": "linear_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_ticket_draft_tree_idx": { + "name": "spec_ticket_draft_tree_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_draft_spec_id_spec_id_fk": { + "name": "spec_ticket_draft_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_ticket_draft_parent_id_fk": { + "name": "spec_ticket_draft_parent_id_fk", + "tableFrom": "spec_ticket_draft", + "tableTo": "spec_ticket_draft", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_config": { + "name": "spec_ticket_sync_config", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_name": { + "name": "project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "label_ids": { + "name": "label_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "label_names": { + "name": "label_names", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_ticket_sync_config_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_config_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_config", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_ticket_sync_operation": { + "name": "spec_ticket_sync_operation", + "schema": "", + "columns": { + "caller_spec_id": { + "name": "caller_spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "reserved_ticket_id": { + "name": "reserved_ticket_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reserved_external_id": { + "name": "reserved_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "spec_ticket_sync_operation_ticket_idx": { + "name": "spec_ticket_sync_operation_ticket_idx", + "columns": [ + { + "expression": "reserved_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_ticket_sync_operation_caller_spec_id_spec_id_fk": { + "name": "spec_ticket_sync_operation_caller_spec_id_spec_id_fk", + "tableFrom": "spec_ticket_sync_operation", + "tableTo": "spec", + "columnsFrom": [ + "caller_spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk": { + "name": "spec_ticket_sync_operation_caller_spec_id_operation_idempotency_key_pk", + "columns": [ + "caller_spec_id", + "operation", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_transcript_action": { + "name": "spec_transcript_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_fingerprint": { + "name": "request_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chip": { + "name": "chip", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "spec_transcript_action_pending_idx": { + "name": "spec_transcript_action_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"spec_transcript_action\".\"delivered_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spec_transcript_action_spec_id_spec_id_fk": { + "name": "spec_transcript_action_spec_id_spec_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spec_transcript_action_actor_user_id_user_id_fk": { + "name": "spec_transcript_action_actor_user_id_user_id_fk", + "tableFrom": "spec_transcript_action", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spec_update_log": { + "name": "spec_update_log", + "schema": "", + "columns": { + "seq": { + "name": "seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spec_id": { + "name": "spec_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "update": { + "name": "update", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "semantic_doc_seq": { + "name": "semantic_doc_seq", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changed_section_ids": { + "name": "changed_section_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spec_update_log_spec_id_spec_id_fk": { + "name": "spec_update_log_spec_id_spec_id_fk", + "tableFrom": "spec_update_log", + "tableTo": "spec", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "spec_update_log_spec_id_seq_pk": { + "name": "spec_update_log_spec_id_seq_pk", + "columns": [ + "spec_id", + "seq" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suggested_title": { + "name": "suggested_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_title": { + "name": "custom_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "harness": { + "name": "harness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_router": { + "name": "model_router", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effort": { + "name": "effort", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_task_id": { + "name": "parent_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "root_task_id": { + "name": "root_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_task_name": { + "name": "local_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_task_name": { + "name": "canonical_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spawning_session_id": { + "name": "spawning_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_policy": { + "name": "launch_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_slack_channel_created_idx": { + "name": "task_slack_channel_created_idx", + "columns": [ + { + "expression": "(source->>'team')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "(source->>'channel')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "source->>'provider' = 'slack'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_parent_idx": { + "name": "task_parent_idx", + "columns": [ + { + "expression": "parent_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_idx": { + "name": "task_root_idx", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_root_canonical_name_unique": { + "name": "task_root_canonical_name_unique", + "columns": [ + { + "expression": "root_task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "canonical_task_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "canonical_task_name IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_session": { + "name": "task_session", + "schema": "", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_grants": { + "name": "integration_grants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_connections": { + "name": "integration_connections", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_principal_id": { + "name": "integration_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_binding_envelope": { + "name": "mcp_binding_envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "integration_snapshot_hash": { + "name": "integration_snapshot_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_session_session_idx": { + "name": "task_session_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_session_task_id_task_id_fk": { + "name": "task_session_task_id_task_id_fk", + "tableFrom": "task_session", + "tableTo": "task", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "task_session_profile_id_profile_id_fk": { + "name": "task_session_profile_id_profile_id_fk", + "tableFrom": "task_session", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "task_session_task_id_session_id_pk": { + "name": "task_session_task_id_session_id_pk", + "columns": [ + "task_id", + "session_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_session_secrets": { + "name": "user_session_secrets", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env_var_name": { + "name": "env_var_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapped_dek": { + "name": "wrapped_dek", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_secrets_user_id_user_id_fk": { + "name": "user_session_secrets_user_id_user_id_fk", + "tableFrom": "user_session_secrets", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_session_secrets_user_id_env_var_name_pk": { + "name": "user_session_secrets_user_id_env_var_name_pk", + "columns": [ + "user_id", + "env_var_name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_registration": { + "name": "webhook_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification": { + "name": "verification", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_hint": { + "name": "provider_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_registration_provider_idx": { + "name": "webhook_registration_provider_idx", + "columns": [ + { + "expression": "provider_hint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_sample": { + "name": "webhook_sample", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_sample_registration_event_idx": { + "name": "webhook_sample_registration_event_idx", + "columns": [ + { + "expression": "registration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_sample_registration_id_webhook_registration_id_fk": { + "name": "webhook_sample_registration_id_webhook_registration_id_fk", + "tableFrom": "webhook_sample", + "tableTo": "webhook_registration", + "columnsFrom": [ + "registration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/orchestrator/drizzle/meta/_journal.json b/orchestrator/drizzle/meta/_journal.json index fac76d50e..03e092183 100644 --- a/orchestrator/drizzle/meta/_journal.json +++ b/orchestrator/drizzle/meta/_journal.json @@ -785,6 +785,20 @@ "when": 1793613661405, "tag": "0111_mcp_completion_receipt", "breakpoints": true + }, + { + "idx": 112, + "version": "7", + "when": 1793616759320, + "tag": "0112_mcp_lifecycle_receipts", + "breakpoints": true + }, + { + "idx": 113, + "version": "7", + "when": 1793619176833, + "tag": "0113_mcp_lifecycle_active", + "breakpoints": true } ] } \ No newline at end of file diff --git a/orchestrator/src/__tests__/mcp-actions-rpc.test.ts b/orchestrator/src/__tests__/mcp-actions-rpc.test.ts new file mode 100644 index 000000000..5c9f09ad6 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-actions-rpc.test.ts @@ -0,0 +1,108 @@ +import { expect, test } from "bun:test"; +import { Code, ConnectError, createClient } from "@connectrpc/connect"; +import { createConnectTransport } from "@connectrpc/connect-node"; +import type { AddressInfo } from "node:net"; +import { Hono } from "hono"; +import { buildServer } from "../server.ts"; +import { IntegrationService } from "../gen/engram/app/v1/integration_pb.ts"; +import { mcpHandlers } from "../mcp/contracts.ts"; +import { reviewContext } from "../mcp/journal-review.ts"; +import { effectFixture } from "./mcp-effect-fixture.ts"; + +test("typed cancellation and unknown resolution require the live owner and exact context", async () => { + const f = await effectFixture({ classification: "write", response: "hold" }); + let actorId = "other"; + const server = buildServer(new Hono(), (router) => + router.service( + IntegrationService, + mcpHandlers({ + services: { + enabled: true, + journal: f.journal, + repository: f.repository, + runtime: { + cancel: async (id) => f.driver.cancel(id), + prepareRebind: async () => { + throw Error("unavailable"); + }, + rebind: async () => { + throw Error("unavailable"); + }, + }, + }, + connections: f.connections, + getSession: async () => ({ + user: { id: actorId, role: "user", email: "user@example.test" }, + }), + now: () => new Date(), + connectionToProto: (row) => ({ id: row.id }), + }), + ), + ); + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(call.id); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); + const pending = f.driver.driveOnce(call.id); + const deadline = Date.now() + 5000; + while (!(await f.command("stats")).calls && Date.now() < deadline) + await new Promise((resolve) => setTimeout(resolve, 10)); + const dispatched = (await f.journal.get(call.id))!; + expect(dispatched.state).toBe("dispatching"); + const url = await new Promise((resolve) => + server.listen(0, "127.0.0.1", () => + resolve(`http://127.0.0.1:${(server.address() as AddressInfo).port}/rpc`), + ), + ); + const client = createClient( + IntegrationService, + createConnectTransport({ baseUrl: url, httpVersion: "1.1" }), + ); + try { + await client.cancelMcpCall({ + executionId: call.id, + expectedJournalRevision: dispatched.version, + }); + throw Error("expected denial"); + } catch (error) { + expect(error).toBeInstanceOf(ConnectError); + expect((error as ConnectError).code).toBe(Code.PermissionDenied); + } + expect((await f.journal.get(call.id))!.state).toBe("dispatching"); + actorId = f.actorId; + const canceled = await client.cancelMcpCall({ + executionId: call.id, + expectedJournalRevision: dispatched.version, + }); + expect(canceled.receipt?.state).toBe("outcome_unknown"); + expect((await pending).state).toBe("outcome_unknown"); + const unknown = (await f.journal.get(call.id))!; + const expected = { + executionId: call.id, + journalRevision: unknown.version, + requestDigest: unknown.requestDigest, + bindingHash: unknown.bindingHash, + intentId: unknown.intentId!, + }; + await expect( + client.resolveMcpOutcome({ + executionId: call.id, + outcome: "acknowledged", + expectedUnknownContext: { ...expected, intentId: "wrong" }, + }), + ).rejects.toThrow(); + const receipt = await client.resolveMcpOutcome({ + executionId: call.id, + outcome: "acknowledged", + expectedUnknownContext: expected, + }); + expect(receipt.receipt?.expectedContext?.intentId).toBe(expected.intentId); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await new Promise((resolve) => + server.listening ? server.close(() => resolve()) : resolve(), + ); + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-active-watch.test.ts b/orchestrator/src/__tests__/mcp-active-watch.test.ts new file mode 100644 index 000000000..e16ddd4be --- /dev/null +++ b/orchestrator/src/__tests__/mcp-active-watch.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from "bun:test"; +import { makeMcpActiveWatch } from "../mcp/active-watch.ts"; + +test("registers before its first authority read and aborts if it fails", async () => { + let registered = false; + const watch = makeMcpActiveWatch({ + check: async () => { + expect(registered).toBe(true); + throw Error("unavailable"); + }, + intervalMs: 10, + }); + const active = watch.register("execution"); + registered = true; + await active.ready; + expect(active.signal.aborted).toBe(true); + active.close(); +}); + +test("periodic authority checks and local cancellation abort active work", async () => { + let calls = 0; + const watch = makeMcpActiveWatch({ + check: async () => { + if (++calls > 1) throw Error("changed"); + }, + intervalMs: 10, + }); + const active = watch.register("execution"); + await active.ready; + await new Promise((resolve) => + active.signal.addEventListener("abort", () => resolve(), { once: true }), + ); + expect(calls).toBeGreaterThan(1); + active.close(); + const other = watch.register("other"); + watch.cancel("other"); + expect(other.signal.aborted).toBe(true); + other.close(); +}); + +test("a stalled authority read aborts within the check budget", async () => { + const watch = makeMcpActiveWatch({ check: () => new Promise(() => {}), intervalMs: 10 }); + const active = watch.register("execution"); + await Promise.race([active.ready, new Promise((resolve) => setTimeout(resolve, 50))]); + expect(active.signal.aborted).toBe(true); + active.close(); +}); diff --git a/orchestrator/src/__tests__/mcp-authority-notification.test.ts b/orchestrator/src/__tests__/mcp-authority-notification.test.ts new file mode 100644 index 000000000..28432ae72 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-authority-notification.test.ts @@ -0,0 +1,63 @@ +import { expect, test } from "bun:test"; +import { effectFixture } from "./mcp-effect-fixture.ts"; +import { makeMcpRepository } from "../mcp/repository.ts"; +import { observeMcpAuthorityChanges } from "../mcp/authority-notification.ts"; + +test("a committed authority mutation requests reconciliation and notification failure does not undo it", async () => { + const { eq } = await import("drizzle-orm"); + const { integrationConnection } = await import("../db/schema.ts"); + const f = await effectFixture({ classification: "read" }); + let calls = 0; + const stop = observeMcpAuthorityChanges(() => { + calls++; + throw Error("unavailable"); + }); + try { + const repository = makeMcpRepository(f.db); + const [current] = await f.db + .select() + .from(integrationConnection) + .where(eq(integrationConnection.id, f.connection.id)); + const updated = await repository.disconnect( + f.connection.id, + current!.authGeneration, + new Date(), + ); + expect(updated.revoked).toBe(true); + expect(calls).toBe(1); + await expect( + repository.disconnect(f.connection.id, current!.authGeneration, new Date()), + ).rejects.toThrow(); + expect(calls).toBe(1); + } finally { + stop(); + await f.cleanup(); + } +}, 20000); + +test("a committed disconnect requests durable reconciliation of active work", async () => { + const { eq } = await import("drizzle-orm"); + const { integrationConnection } = await import("../db/schema.ts"); + const { makeMcpLifecycleReconciler } = await import("../mcp/lifecycle-reconciler.ts"); + const f = await effectFixture({ classification: "read" }); + const scanner = makeMcpLifecycleReconciler({ db: f.db, driver: f.driver }); + let scan: Promise | undefined; + const stop = observeMcpAuthorityChanges(() => { + scan = scanner.reconcileAuthorityChange(); + }); + try { + const call = await f.driver.acceptToolRequest(f.request); + const [current] = await f.db + .select() + .from(integrationConnection) + .where(eq(integrationConnection.id, f.connection.id)); + await makeMcpRepository(f.db).disconnect(f.connection.id, current!.authGeneration, new Date()); + expect(scan).toBeDefined(); + await scan; + expect((await f.journal.get(call.id))!.state).toBe("canceled"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + stop(); + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-dbos-restart.test.ts b/orchestrator/src/__tests__/mcp-dbos-restart.test.ts index 8347248db..77f73e6d6 100644 --- a/orchestrator/src/__tests__/mcp-dbos-restart.test.ts +++ b/orchestrator/src/__tests__/mcp-dbos-restart.test.ts @@ -1,3 +1,4 @@ +import { unknownContext } from "../mcp/journal.ts"; import { beforeAll, afterAll, expect, test } from "bun:test"; import { spawn } from "node:child_process"; import { createInterface } from "node:readline"; @@ -403,7 +404,13 @@ test("late verified outcome after expiry gets a new completion receipt without a expect(unknown.completionEnqueuedVersion).toBe(unknown.version); f.requests.set("new-write", { ...f.request, toolCallId: "new-write" }); await expect(f.driver.acceptToolRequest(f.requests.get("new-write")!)).rejects.toThrow(); - await f.journal.resolveUnknown(dispatched.id, f.actorId, "acknowledged", new Date()); + await f.journal.resolveUnknown( + dispatched.id, + f.actorId, + "acknowledged", + new Date(), + unknownContext((await f.journal.get(dispatched.id))!), + ); const resolved = (await callFor(f))!; expect(resolved.state).toBe("outcome_unknown"); expect(resolved.result).toBeNull(); diff --git a/orchestrator/src/__tests__/mcp-effect-driver.test.ts b/orchestrator/src/__tests__/mcp-effect-driver.test.ts index ad6f4cb8e..7efac08eb 100644 --- a/orchestrator/src/__tests__/mcp-effect-driver.test.ts +++ b/orchestrator/src/__tests__/mcp-effect-driver.test.ts @@ -434,7 +434,7 @@ test("cancel after provider send leaves an unknown write that cannot replay", as while (!(await f.command("stats")).calls && Date.now() < until) await new Promise((resolve) => setTimeout(resolve, 10)); expect((await f.command("stats")).calls).toBe(1); - await f.journal.cancel(call.id, f.actorId, new Date()); + await f.journal.cancel(call.id, f.actorId, new Date(), (await f.journal.get(call.id))!.version); controller.abort(); expect((await pending).state).toBe("outcome_unknown"); await f.driver.driveOnce(call.id); @@ -611,3 +611,158 @@ test("credential delivery expiry during only the final fence prevents send and r await f.cleanup(); } }, 20000); + +test("durable cancellation aborts an active provider call without a notification", async () => { + const f = await effectFixture({ classification: "write", response: "hold" }); + const controller = new AbortController(); + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(call.id); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()); + const pending = f.driver.driveOnce(call.id, controller.signal); + const until = Date.now() + 5000; + while (!(await f.command("stats")).calls && Date.now() < until) + await new Promise((resolve) => setTimeout(resolve, 10)); + expect((await f.command("stats")).calls).toBe(1); + const dispatched = (await f.journal.get(call.id))!; + await f.journal.cancel(call.id, f.actorId, new Date(), dispatched.version); + const result = await Promise.race([ + pending, + new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 2500)), + ]); + expect(result).not.toBe("timeout"); + if (result !== "timeout") expect(result.state).toBe("outcome_unknown"); + expect((await f.command("stats")).calls).toBe(1); + } finally { + controller.abort(); + await f.cleanup(); + } +}, 20000); + +test("canceling a dispatched read does not invent an upstream failure", async () => { + const f = await effectFixture({ classification: "read", response: "hold" }); + const controller = new AbortController(); + try { + const call = await f.driver.acceptToolRequest(f.request); + const pending = f.driver.driveOnce(call.id, controller.signal); + const until = Date.now() + 5000; + while (!(await f.command("stats")).calls && Date.now() < until) + await new Promise((resolve) => setTimeout(resolve, 10)); + expect((await f.command("stats")).calls).toBe(1); + const dispatched = (await f.journal.get(call.id))!; + await f.journal.cancel(call.id, f.actorId, new Date(), dispatched.version); + const result = await Promise.race([ + pending, + new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 2500)), + ]); + expect(result).not.toBe("timeout"); + if (result !== "timeout") expect(result.state).toBe("outcome_unknown"); + } finally { + controller.abort(); + await f.cleanup(); + } +}, 20000); + +test("lifecycle reconciliation preserves a current newer execution", async () => { + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.reconcile(call.id); + expect((await f.journal.get(call.id))!.state).toBe("authorized"); + expect((await f.driver.driveOnce(call.id)).state).toBe("succeeded"); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await f.cleanup(); + } +}, 20000); + +test("lifecycle reconciliation cancels an old run before it can dispatch", async () => { + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + f.setFence(false); + await f.driver.reconcile(call.id); + expect((await f.journal.get(call.id))!.state).toBe("canceled"); + expect((await f.driver.driveOnce(call.id)).state).toBe("canceled"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("lifecycle reconciliation cancels after the principal loses access", async () => { + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.db.update(user).set({ banned: true }).where(eq(user.id, f.actorId)); + await f.driver.reconcile(call.id); + expect((await f.journal.get(call.id))!.state).toBe("canceled"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("unavailable coordinator fence leaves execution pending for retry", async () => { + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + f.beforeFence(async () => { + throw Error("temporary control outage"); + }); + await expect(f.driver.reconcile(call.id)).rejects.toThrow(); + expect((await f.journal.get(call.id))!.state).toBe("authorized"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("an older resume reconciles current authority without canceling a newer run", async () => { + const { makeMcpLifecycleEvents } = await import("../mcp/lifecycle-events.ts"); + const { consumerCursor } = await import("../db/schema.ts"); + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + const lifecycle = makeMcpLifecycleEvents({ + db: f.db, + now: () => new Date(), + list: async (_id, after) => + after < 1n ? [{ idx: 1n, kind: "resumed", payloadJson: "{}" }] : [], + reconcile: async () => f.driver.reconcile(call.id), + }); + await lifecycle.runOnce(f.sessionId); + expect((await f.journal.get(call.id))!.state).toBe("authorized"); + expect((await f.driver.driveOnce(call.id)).state).toBe("succeeded"); + expect((await f.command("stats")).calls).toBe(1); + } finally { + await f.db.delete(consumerCursor).where(eq(consumerCursor.sessionId, f.sessionId)); + await f.cleanup(); + } +}, 20000); + +test("a late old-run acceptance is canceled by independent reconciliation", async () => { + const { makeMcpLifecycleEvents } = await import("../mcp/lifecycle-events.ts"); + const { consumerCursor } = await import("../db/schema.ts"); + const f = await effectFixture({ classification: "read" }); + try { + const lifecycle = makeMcpLifecycleEvents({ + db: f.db, + now: () => new Date(), + list: async (_id, after) => + after < 1n ? [{ idx: 1n, kind: "run_interrupted", payloadJson: '{"run_id":"run-1"}' }] : [], + reconcile: async () => {}, + }); + await lifecycle.runOnce(f.sessionId); + const call = await f.driver.acceptToolRequest(f.request); + f.setFence(false); + await f.driver.reconcile(call.id); + expect((await f.journal.get(call.id))!.state).toBe("canceled"); + expect((await f.driver.driveOnce(call.id)).state).toBe("canceled"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.db.delete(consumerCursor).where(eq(consumerCursor.sessionId, f.sessionId)); + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-lifecycle-actions.test.ts b/orchestrator/src/__tests__/mcp-lifecycle-actions.test.ts new file mode 100644 index 000000000..a398446e6 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-lifecycle-actions.test.ts @@ -0,0 +1,206 @@ +import { expect, test } from "bun:test"; +import { schemaVerifiedFixture } from "./mcp-review-fixture.ts"; +import { reviewContext } from "../mcp/journal-review.ts"; +import { persistVerifiedOutcome } from "../mcp/effect-driver.ts"; + +async function dispatch(f: Awaited>) { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const approved = ( + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), f.now) + ).call; + await f.journal.admit(call.id, f.deploymentId, f.now); + const lease = (await f.journal.getAdmission(call.id))!; + return f.journal.dispatch( + call.id, + { bootId: "boot", intentId: crypto.randomUUID(), expiresAt: lease.expiresAt }, + f.now, + { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: approved.version, + reservationId: lease.reservationId, + reservationExpiresAt: lease.expiresAt, + }, + ); +} + +test("cancel requires the observed revision and returns a durable receipt without undo", async () => { + const f = await schemaVerifiedFixture(); + try { + const call = await dispatch(f); + await expect(f.journal.cancel(call.id, f.actorId, f.now, call.version - 1)).rejects.toThrow( + "stale", + ); + expect((await f.journal.get(call.id))!.state).toBe("dispatching"); + const canceled = await f.journal.cancel(call.id, f.actorId, f.now, call.version); + expect(canceled.state).toBe("outcome_unknown"); + const data = await f.journal.readSession(f.sessionId, f.actorId, { limit: 16 }); + expect(data.rows[0]!.cancellation).toMatchObject({ + actorId: f.actorId, + expectedJournalRevision: call.version, + journalRevision: call.version + 1, + state: "outcome_unknown", + }); + expect(await f.journal.hasUnknown(f.sessionId, f.connection.id)).toBe(true); + } finally { + await f.cleanup(); + } +}); + +test("resolution requires exact unknown context and retains later verified truth", async () => { + const f = await schemaVerifiedFixture(); + try { + const dispatched = await dispatch(f); + const call = await f.journal.cancel(dispatched.id, f.actorId, f.now, dispatched.version); + const expected = { + executionId: call.id, + journalRevision: call.version, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + intentId: call.intentId!, + }; + await expect( + f.journal.resolveUnknown(call.id, f.actorId, "acknowledged", f.now, { + ...expected, + intentId: "other", + }), + ).rejects.toThrow("stale"); + expect(await f.journal.hasUnknown(f.sessionId, f.connection.id)).toBe(true); + const receipt = await f.journal.resolveUnknown( + call.id, + f.actorId, + "acknowledged", + f.now, + expected, + ); + expect(receipt.expectedContext).toEqual(expected); + expect(await f.journal.hasUnknown(f.sessionId, f.connection.id)).toBe(false); + const result = await persistVerifiedOutcome( + f.journal, + dispatched, + { + state: "succeeded", + result: { outcome: "succeeded", delivery: "available", output: { content: [] } }, + resultUnavailable: false, + }, + () => f.now, + ); + expect(result.state).toBe("succeeded"); + expect(result.intentId).toBe(dispatched.intentId); + const data = await f.journal.readSession(f.sessionId, f.actorId, { limit: 16 }); + expect(data.rows[0]!.resolution).toEqual(receipt); + expect(data.rows[0]!.call.result).toEqual(result.result); + } finally { + await f.cleanup(); + } +}); + +test("verified result before resolution makes the unknown context stale", async () => { + const f = await schemaVerifiedFixture(); + try { + const dispatched = await dispatch(f); + const unknown = await f.journal.cancel(dispatched.id, f.actorId, f.now, dispatched.version); + const expected = { + executionId: unknown.id, + journalRevision: unknown.version, + requestDigest: unknown.requestDigest, + bindingHash: unknown.bindingHash, + intentId: unknown.intentId!, + }; + const result = await persistVerifiedOutcome( + f.journal, + dispatched, + { + state: "succeeded", + result: { outcome: "succeeded", delivery: "available", output: { content: [] } }, + resultUnavailable: false, + }, + () => f.now, + ); + expect(result.state).toBe("succeeded"); + await expect( + f.journal.resolveUnknown(unknown.id, f.actorId, "acknowledged", f.now, expected), + ).rejects.toThrow("stale"); + const data = await f.journal.readSession(f.sessionId, f.actorId, { limit: 16 }); + expect(data.rows[0]!.resolution).toBeNull(); + expect(data.rows[0]!.call.result).toEqual(result.result); + } finally { + await f.cleanup(); + } +}); + +test("concurrent resolution and verified result preserve the committed evidence", async () => { + const f = await schemaVerifiedFixture(); + try { + const dispatched = await dispatch(f); + const unknown = await f.journal.cancel(dispatched.id, f.actorId, f.now, dispatched.version); + const expected = { + executionId: unknown.id, + journalRevision: unknown.version, + requestDigest: unknown.requestDigest, + bindingHash: unknown.bindingHash, + intentId: unknown.intentId!, + }; + const [resolution, result] = await Promise.allSettled([ + f.journal.resolveUnknown(unknown.id, f.actorId, "acknowledged", f.now, expected), + persistVerifiedOutcome( + f.journal, + dispatched, + { + state: "succeeded", + result: { outcome: "succeeded", delivery: "available", output: { content: [] } }, + resultUnavailable: false, + }, + () => f.now, + ), + ]); + expect(result.status).toBe("fulfilled"); + const data = await f.journal.readSession(f.sessionId, f.actorId, { limit: 16 }); + expect(data.rows[0]!.call.state).toBe("succeeded"); + expect(data.rows[0]!.call.result).toMatchObject({ outcome: "succeeded" }); + if (resolution.status === "fulfilled") + expect(data.rows[0]!.resolution).toEqual(resolution.value); + else { + expect(String(resolution.reason)).toContain("stale"); + expect(data.rows[0]!.resolution).toBeNull(); + } + } finally { + await f.cleanup(); + } +}); + +test("an unverified read failure cannot overwrite a concurrent cancellation", async () => { + const f = await schemaVerifiedFixture(); + try { + const dispatched = await dispatch(f); + let first = true; + const journal = { + ...f.journal, + async get(id: string) { + const before = await f.journal.get(id); + if (first) { + first = false; + await f.journal.cancel(id, f.actorId, f.now, dispatched.version); + } + return before; + }, + }; + const result = await persistVerifiedOutcome( + journal, + dispatched, + { + state: "failed", + result: { outcome: "failed", delivery: "unavailable" }, + resultUnavailable: false, + }, + () => f.now, + false, + ); + expect(result.state).toBe("outcome_unknown"); + } finally { + await f.cleanup(); + } +}); diff --git a/orchestrator/src/__tests__/mcp-lifecycle-cursor.test.ts b/orchestrator/src/__tests__/mcp-lifecycle-cursor.test.ts new file mode 100644 index 000000000..86d5c0d79 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-lifecycle-cursor.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from "bun:test"; +import { eq } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { consumerCursor } from "../db/schema.ts"; +import { makeMcpLifecycleEvents } from "../mcp/lifecycle-events.ts"; + +test("a failed event cannot be skipped and competing replicas advance only after cancellation", async () => { + const db = getDb(); + const sessionId = crypto.randomUUID(); + const events = [ + { idx: 1n, kind: "run_interrupted", payloadJson: "{}" }, + { idx: 2n, kind: "resumed", payloadJson: "{}" }, + ]; + let fail = true; + const canceled: bigint[] = []; + const make = () => + makeMcpLifecycleEvents({ + db, + now: () => new Date(), + list: async (_id, after) => events.filter((event) => event.idx > after), + reconcile: async (_id, event) => { + if (event.idx === 1n && fail) throw Error("unavailable"); + canceled.push(event.idx); + }, + }); + try { + await expect(make().runOnce(sessionId)).rejects.toThrow("unavailable"); + expect( + (await db.select().from(consumerCursor).where(eq(consumerCursor.sessionId, sessionId)))[0]! + .lastIdx, + ).toBe(-1n); + expect(canceled).toEqual([]); + fail = false; + await Promise.all([make().runOnce(sessionId), make().runOnce(sessionId)]); + await make().runOnce(sessionId); + expect( + (await db.select().from(consumerCursor).where(eq(consumerCursor.sessionId, sessionId)))[0]! + .lastIdx, + ).toBe(2n); + expect(canceled).toContain(1n); + expect(canceled).toContain(2n); + expect(canceled.indexOf(1n)).toBeLessThan(canceled.indexOf(2n)); + } finally { + await db.delete(consumerCursor).where(eq(consumerCursor.sessionId, sessionId)); + } +}); + +test("invalid event order cannot advance beyond the last committed event", async () => { + const db = getDb(); + const sessionId = crypto.randomUUID(); + const reader = makeMcpLifecycleEvents({ + db, + now: () => new Date(), + list: async () => [ + { idx: 3n, kind: "run_interrupted", payloadJson: "{}" }, + { idx: 2n, kind: "resumed", payloadJson: "{}" }, + ], + reconcile: async () => {}, + }); + try { + await expect(reader.runOnce(sessionId)).rejects.toThrow("order"); + const [cursor] = await db + .select() + .from(consumerCursor) + .where(eq(consumerCursor.sessionId, sessionId)); + expect(cursor!.lastIdx).toBe(-1n); + } finally { + await db.delete(consumerCursor).where(eq(consumerCursor.sessionId, sessionId)); + } +}); diff --git a/orchestrator/src/__tests__/mcp-lifecycle-interceptor.test.ts b/orchestrator/src/__tests__/mcp-lifecycle-interceptor.test.ts new file mode 100644 index 000000000..7ec3e1469 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-lifecycle-interceptor.test.ts @@ -0,0 +1,65 @@ +import { expect, test } from "bun:test"; +import { createContextValues, type UnaryRequest, type UnaryResponse } from "@connectrpc/connect"; +import { create } from "@bufbuild/protobuf"; +import { + InterruptRequestSchema, + InterruptResponseSchema, + SessionService, +} from "../gen/engram/app/v1/session_pb.ts"; +import { makeMcpLifecycleInterceptor } from "../mcp/lifecycle-interceptor.ts"; + +test("trusted interrupt waits for durable invalidation before forwarding", async () => { + const order: string[] = []; + const req: UnaryRequest = { + stream: false, + service: SessionService, + method: SessionService.method.interrupt, + requestMethod: "POST", + url: "http://example.test", + signal: new AbortController().signal, + header: new Headers(), + contextValues: createContextValues(), + message: create(InterruptRequestSchema, { sessionId: "s1" }), + }; + const res: UnaryResponse = { + stream: false, + service: SessionService, + method: SessionService.method.interrupt, + header: new Headers(), + trailer: new Headers(), + message: create(InterruptResponseSchema), + }; + const interceptor = makeMcpLifecycleInterceptor(async (id, cause) => { + order.push(`${id}:${cause}`); + }); + await interceptor(async () => { + order.push("forward"); + return res; + })(req); + expect(order).toEqual(["s1:interrupt", "forward"]); +}); + +test("invalidation failure stops the lifecycle request", async () => { + const req: UnaryRequest = { + stream: false, + service: SessionService, + method: SessionService.method.interrupt, + requestMethod: "POST", + url: "http://example.test", + signal: new AbortController().signal, + header: new Headers(), + contextValues: createContextValues(), + message: create(InterruptRequestSchema, { sessionId: "s1" }), + }; + let forwarded = false; + const interceptor = makeMcpLifecycleInterceptor(async () => { + throw Error("store unavailable"); + }); + await expect( + interceptor(async () => { + forwarded = true; + throw Error("unexpected forward"); + })(req), + ).rejects.toThrow("store unavailable"); + expect(forwarded).toBe(false); +}); diff --git a/orchestrator/src/__tests__/mcp-lifecycle-invalidation.test.ts b/orchestrator/src/__tests__/mcp-lifecycle-invalidation.test.ts new file mode 100644 index 000000000..97bf1171b --- /dev/null +++ b/orchestrator/src/__tests__/mcp-lifecycle-invalidation.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from "bun:test"; +import { effectFixture } from "./mcp-effect-fixture.ts"; +import { makeMcpLifecycleInvalidator } from "../mcp/lifecycle-invalidation.ts"; + +test("internal lifecycle cancellation commits before local abort", async () => { + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + const invalidator = makeMcpLifecycleInvalidator({ + db: f.db, + abort: async (id) => { + expect((await f.journal.get(id))!.state).toBe("canceled"); + }, + }); + await invalidator(f.sessionId, "interrupt"); + expect((await f.journal.cancellationReceipt(call.id))?.cause).toBe("interrupt"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-lifecycle-reconciler.test.ts b/orchestrator/src/__tests__/mcp-lifecycle-reconciler.test.ts new file mode 100644 index 000000000..9a9e3c622 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-lifecycle-reconciler.test.ts @@ -0,0 +1,184 @@ +import { expect, test } from "bun:test"; +import { effectFixture } from "./mcp-effect-fixture.ts"; +import { makeMcpLifecycleReconciler } from "../mcp/lifecycle-reconciler.ts"; + +test("independent keyset scan cancels stale accepted work without provider replay", async () => { + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + f.setFence(false); + const scanner = makeMcpLifecycleReconciler({ db: f.db, driver: f.driver }); + await scanner.runOnce(); + expect((await f.journal.get(call.id))!.state).toBe("canceled"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("recovery lifecycle pass runs event catch-up and independent currentness", async () => { + const { makeMcpRecovery } = await import("../mcp/recovery.ts"); + const { makeMcpRecoveryScanner } = await import("../mcp/recovery-scanner.ts"); + const { consumerCursor } = await import("../db/schema.ts"); + const { eq } = await import("drizzle-orm"); + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + listLifecycleEvents: async (_id, after) => + after < 1n ? [{ idx: 1n, kind: "resumed", payloadJson: "{}" }] : [], + }); + const scanner = makeMcpRecoveryScanner({ + deploymentId: f.deploymentId, + runtime: () => runtime, + }); + await scanner.runLifecycleOnce(); + expect((await f.journal.get(call.id))!.state).toBe("authorized"); + expect( + ( + await f.db.select().from(consumerCursor).where(eq(consumerCursor.sessionId, f.sessionId)) + )[0]!.lastIdx, + ).toBe(1n); + f.setFence(false); + await scanner.runLifecycleOnce(); + expect((await f.journal.get(call.id))!.state).toBe("canceled"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + await f.db.delete(consumerCursor).where(eq(consumerCursor.sessionId, f.sessionId)); + await f.cleanup(); + } +}, 20000); + +test("a stalled authority check times out and aborts local I/O", async () => { + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + const canceled: string[] = []; + const scanner = makeMcpLifecycleReconciler({ + db: f.db, + timeoutMs: 10, + driver: { + reconcile: () => new Promise(() => {}), + cancel: (id) => { + canceled.push(id); + }, + }, + }); + await expect(scanner.runOnce()).rejects.toThrow("unavailable"); + expect(canceled).toContain(call.id); + expect((await f.journal.get(call.id))!.state).toBe("authorized"); + } finally { + await f.cleanup(); + } +}, 20000); + +test("a timed-out stale check cannot cancel later when its response arrives", async () => { + const f = await effectFixture({ classification: "read" }); + let release!: () => void; + const blocked = new Promise((resolve) => { + release = resolve; + }); + try { + const call = await f.driver.acceptToolRequest(f.request); + f.setFence(false); + f.beforeFence(() => blocked); + const scanner = makeMcpLifecycleReconciler({ db: f.db, driver: f.driver, timeoutMs: 10 }); + await expect(scanner.runOnce()).rejects.toThrow("unavailable"); + release(); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect((await f.journal.get(call.id))!.state).toBe("authorized"); + } finally { + release(); + await f.cleanup(); + } +}, 20000); + +test("the recovery scheduler runs lifecycle catch-up without a workflow delivery", async () => { + const { makeMcpRecovery } = await import("../mcp/recovery.ts"); + const { makeMcpRecoveryScanner } = await import("../mcp/recovery-scanner.ts"); + const { consumerCursor } = await import("../db/schema.ts"); + const { eq } = await import("drizzle-orm"); + const f = await effectFixture({ classification: "read" }); + let scanner: ReturnType | undefined; + try { + const call = await f.driver.acceptToolRequest(f.request); + const runtime = makeMcpRecovery({ + db: f.db, + driver: f.driver, + deploymentId: f.deploymentId, + now: () => new Date(), + listLifecycleEvents: async (_id, after) => + after < 1n ? [{ idx: 1n, kind: "resumed", payloadJson: "{}" }] : [], + }); + scanner = makeMcpRecoveryScanner({ + deploymentId: f.deploymentId, + journal: f.journal, + runtime: () => runtime, + startWorkflow: async () => {}, + }); + await scanner.start(); + const deadline = Date.now() + 3000; + while (Date.now() < deadline) { + const [current] = await f.db + .select() + .from(consumerCursor) + .where(eq(consumerCursor.sessionId, f.sessionId)); + if (current?.lastIdx === 1n) break; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + const [cursor] = await f.db + .select() + .from(consumerCursor) + .where(eq(consumerCursor.sessionId, f.sessionId)); + expect(cursor?.lastIdx).toBe(1n); + expect((await f.journal.get(call.id))!.state).toBe("authorized"); + } finally { + await scanner?.stop(); + await f.db.delete(consumerCursor).where(eq(consumerCursor.sessionId, f.sessionId)); + await f.cleanup(); + } +}, 20000); + +test("active keyset uses the partial index and advances past a full page", async () => { + const { eq, sql } = await import("drizzle-orm"); + const { mcpExecution } = await import("../db/schema.ts"); + const { MCP_LIMITS } = await import("../mcp/limits.ts"); + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + const [base] = await f.db.select().from(mcpExecution).where(eq(mcpExecution.id, call.id)); + const copies = Array.from({ length: MCP_LIMITS.queueSize }, (_, i) => ({ + ...base!, + id: crypto.randomUUID(), + toolCallId: `scan-${i}-${crypto.randomUUID()}`, + })); + await f.db.insert(mcpExecution).values(copies); + const seen = new Set(); + const scanner = makeMcpLifecycleReconciler({ + db: f.db, + driver: { + async reconcile(id) { + seen.add(id); + }, + cancel() {}, + }, + }); + await scanner.runOnce(); + expect(seen.size).toBe(MCP_LIMITS.queueSize); + await scanner.runOnce(); + expect(seen.size).toBe(MCP_LIMITS.queueSize + 1); + const plan = await f.db.transaction(async (tx) => { + await tx.execute(sql`SET LOCAL enable_seqscan = off`); + return tx.execute( + sql`EXPLAIN SELECT id FROM mcp_execution WHERE state IN ('waiting_approval','authorized','dispatching') AND id > '' ORDER BY id LIMIT 128`, + ); + }); + expect(JSON.stringify(plan.rows)).toContain("mcp_execution_lifecycle_active_idx"); + } finally { + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-lifecycle-rpc.test.ts b/orchestrator/src/__tests__/mcp-lifecycle-rpc.test.ts new file mode 100644 index 000000000..026c879fd --- /dev/null +++ b/orchestrator/src/__tests__/mcp-lifecycle-rpc.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from "bun:test"; +import { Code, ConnectError, createClient, createRouterTransport } from "@connectrpc/connect"; +import { createConnectTransport } from "@connectrpc/connect-node"; +import type { AddressInfo } from "node:net"; +import { Hono } from "hono"; +import { buildServer } from "../server.ts"; +import { SessionService } from "../gen/engram/app/v1/session_pb.ts"; +import { registerPassthrough } from "../rpc/passthrough.ts"; +import { makeMcpLifecycleInterceptor } from "../mcp/lifecycle-interceptor.ts"; +import { makeMcpLifecycleInvalidator } from "../mcp/lifecycle-invalidation.ts"; +import { effectFixture } from "./mcp-effect-fixture.ts"; + +test("public interrupt authorizes before durable cancellation and upstream forwarding", async () => { + const f = await effectFixture({ classification: "read" }); + const call = await f.driver.acceptToolRequest(f.request); + let owner = "other-owner"; + let forwarded = 0; + const invalidate = makeMcpLifecycleInvalidator({ + db: f.db, + abort: async (id) => f.driver.cancel(id), + }); + const upstream = createRouterTransport( + (router) => + router.service(SessionService, { + async interrupt(req) { + expect((await f.journal.get(call.id))!.state).toBe("canceled"); + forwarded++; + return { sessionId: req.sessionId }; + }, + }), + { transport: { interceptors: [makeMcpLifecycleInterceptor(invalidate)] } }, + ); + const server = buildServer(new Hono(), (router) => + registerPassthrough( + router, + [{ service: SessionService, methods: ["Interrupt"] }], + upstream, + async () => ({ user: { id: f.actorId, role: "user", email: "user@example.test" } }), + async () => owner, + ), + ); + try { + const url = await new Promise((resolve) => + server.listen(0, "127.0.0.1", () => + resolve(`http://127.0.0.1:${(server.address() as AddressInfo).port}/rpc`), + ), + ); + const client = createClient( + SessionService, + createConnectTransport({ baseUrl: url, httpVersion: "1.1" }), + ); + try { + await client.interrupt({ sessionId: f.sessionId }); + throw Error("expected denial"); + } catch (error) { + expect(error).toBeInstanceOf(ConnectError); + expect((error as ConnectError).code).toBe(Code.NotFound); + } + expect((await f.journal.get(call.id))!.state).toBe("authorized"); + expect(forwarded).toBe(0); + owner = f.actorId; + await client.interrupt({ sessionId: f.sessionId }); + expect(forwarded).toBe(1); + expect((await f.journal.cancellationReceipt(call.id))?.cause).toBe("interrupt"); + } finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-preflight-rejection.test.ts b/orchestrator/src/__tests__/mcp-preflight-rejection.test.ts index 4e54ab0ca..0f1edf7fe 100644 --- a/orchestrator/src/__tests__/mcp-preflight-rejection.test.ts +++ b/orchestrator/src/__tests__/mcp-preflight-rejection.test.ts @@ -87,7 +87,13 @@ test("a verified late success survives cancellation between result read and resu { ...f.journal, async finish(...args) { - if (++commits === 1) await f.journal.cancel(call.id, f.actorId, f.now); + if (++commits === 1) + await f.journal.cancel( + call.id, + f.actorId, + f.now, + (await f.journal.get(call.id))!.version, + ); return f.journal.finish(...args); }, }, diff --git a/orchestrator/src/__tests__/mcp-rebind-deny.test.ts b/orchestrator/src/__tests__/mcp-rebind-deny.test.ts new file mode 100644 index 000000000..36ce4644f --- /dev/null +++ b/orchestrator/src/__tests__/mcp-rebind-deny.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from "bun:test"; +import { canonicalDigest } from "../mcp/hash.ts"; +import { prepareRebindDeny } from "../mcp/rebind-deny.ts"; + +function policy(revision: number, hosts: string[]) { + const body = { version: 1 as const, revision, hosts }; + return { ...body, digest: canonicalDigest(body) }; +} + +test("lost acknowledgement retains the host-applied deny set at a newer revision", () => { + const old = policy(2, ["old.example.test"]); + const next = policy(3, ["new.example.test", "parent.example.test"]); + const applied = policy(3, ["lost.example.test", "old.example.test"]); + const host = { + sessionId: "s1", + bindingEpoch: 7, + bindingHash: `sha256:${"a".repeat(64)}`, + policy: applied, + }; + const result = prepareRebindDeny({ sessionId: "s1", bindingEpoch: 7, old, next, host }); + expect(result.revision).toBe(4); + expect(result.hosts).toEqual([ + "lost.example.test", + "new.example.test", + "old.example.test", + "parent.example.test", + ]); + expect(result.digest).toBe(canonicalDigest({ version: 1, revision: 4, hosts: result.hosts })); +}); + +test("untrusted host identity and corrupted applied policy fail closed", () => { + const old = policy(1, ["old.example.test"]); + const host = { + sessionId: "s1", + bindingEpoch: 7, + bindingHash: `sha256:${"a".repeat(64)}`, + policy: old, + }; + expect(() => + prepareRebindDeny({ sessionId: "s1", bindingEpoch: 8, old, next: old, host }), + ).toThrow(); + expect(() => + prepareRebindDeny({ + sessionId: "s1", + bindingEpoch: 7, + old, + next: old, + host: { ...host, policy: { ...old, hosts: [] } }, + }), + ).toThrow(); +}); + +test("host apply is compare-and-set and validates the exact acknowledged binding", async () => { + const { applyRebindDeny } = await import("../mcp/rebind-deny.ts"); + const old = policy(1, ["old.example.test"]); + const host = { + sessionId: "s1", + bindingEpoch: 7, + bindingHash: `sha256:${"a".repeat(64)}`, + policy: old, + }; + const next = prepareRebindDeny({ sessionId: "s1", bindingEpoch: 7, old, next: old, host }); + const expected = { + sessionId: "s1", + bindingEpoch: 7, + bindingHash: `sha256:${"b".repeat(64)}`, + denyRevision: next.revision, + denyDigest: next.digest, + }; + let installed = host; + const fixture = { + async compareAndApply(previous: typeof host, desired: typeof expected, deny: typeof next) { + if (canonicalDigest(previous) !== canonicalDigest(installed)) + throw Error("host state changed"); + installed = { ...host, bindingHash: desired.bindingHash, policy: deny }; + return desired; + }, + }; + await applyRebindDeny(fixture, host, expected, next); + await expect(applyRebindDeny(fixture, host, expected, next)).rejects.toThrow( + "host state changed", + ); + const wrongAck = { + ...fixture, + async compareAndApply() { + return { ...expected, bindingHash: host.bindingHash }; + }, + }; + await expect(applyRebindDeny(wrongAck, installed, expected, next)).rejects.toThrow(); +}); diff --git a/orchestrator/src/__tests__/mcp-rebind-dispatch-race.test.ts b/orchestrator/src/__tests__/mcp-rebind-dispatch-race.test.ts new file mode 100644 index 000000000..e959036a8 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-rebind-dispatch-race.test.ts @@ -0,0 +1,417 @@ +import { expect, test } from "bun:test"; +import { effectFixture } from "./mcp-effect-fixture.ts"; +import { makeMcpRebind } from "../mcp/rebind.ts"; + +test("a rebind cannot expose a new binding while the old dispatch is in its final fence", async () => { + const f = await effectFixture({ classification: "read" }); + let unblock!: () => void; + const blocked = new Promise((resolve) => { + unblock = resolve; + }); + let notify!: () => void; + const entered = new Promise((resolve) => { + notify = resolve; + }); + try { + const call = await f.driver.acceptToolRequest(f.request); + let afterLeaseFences = 0; + f.beforeFence(async () => { + if (f.leases > 0 && ++afterLeaseFences === 2) { + notify(); + await blocked; + } + }); + const pending = f.driver.driveOnce(call.id); + await entered; + let hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + expect(candidate.drain?.reason).toBe("dispatch_in_progress"); + expect(candidate.drain?.retryAfter).toBeTruthy(); + const { mcpOperation } = await import("../mcp/contracts.ts"); + const { McpRebindDrainSchema, McpRebindDrainReason } = + await import("../gen/engram/app/v1/integration_pb.ts"); + const { ConnectError } = await import("@connectrpc/connect"); + try { + await mcpOperation(() => + service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId), + ); + throw Error("expected drain"); + } catch (error) { + expect(error).toBeInstanceOf(ConnectError); + expect( + (error as InstanceType).findDetails(McpRebindDrainSchema)[0]?.reason, + ).toBe(McpRebindDrainReason.DISPATCH_IN_PROGRESS); + } + const attempt = await service + .commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId) + .then( + () => "committed", + (error: Error) => error.message, + ); + unblock(); + await pending; + expect(attempt).toContain("dispatch in progress"); + expect((await f.command("stats")).calls).toBe(1); + await service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId); + expect(hostState.bindingHash).toBe(candidate.bindingHash); + } finally { + unblock(); + await f.cleanup(); + } +}, 20000); + +test("cancel and manual resolution do not waive the original in-flight intent", async () => { + const f = await effectFixture({ classification: "read" }); + let unblock!: () => void; + const blocked = new Promise((resolve) => { + unblock = resolve; + }); + let notify!: () => void; + const entered = new Promise((resolve) => { + notify = resolve; + }); + try { + const call = await f.driver.acceptToolRequest(f.request); + let afterLeaseFences = 0; + f.beforeFence(async () => { + if (f.leases > 0 && ++afterLeaseFences === 2) { + notify(); + await blocked; + } + }); + const pending = f.driver.driveOnce(call.id); + await entered; + const dispatched = (await f.journal.get(call.id))!; + const unknown = await f.journal.cancel(call.id, f.actorId, new Date(), dispatched.version); + const context = { + executionId: call.id, + journalRevision: unknown.version, + requestDigest: unknown.requestDigest, + bindingHash: unknown.bindingHash, + intentId: unknown.intentId!, + }; + await f.journal.resolveUnknown(call.id, f.actorId, "acknowledged", new Date(), context); + let hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + expect(candidate.drain?.reason).toBe("unknown_intent"); + expect(candidate.drain?.retryAfter).toBeTruthy(); + const attempt = await service + .commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId) + .then( + () => "committed", + (error: Error) => error.message, + ); + unblock(); + await pending; + expect(attempt).toContain("dispatch in progress"); + expect((await f.command("stats")).calls).toBe(1); + await service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId); + expect(hostState.bindingHash).toBe(candidate.bindingHash); + } finally { + unblock(); + await f.cleanup(); + } +}, 20000); + +test("an expired original intent permits rebind and cannot send afterward", async () => { + const f = await effectFixture({ classification: "read" }); + let unblock!: () => void; + const blocked = new Promise((resolve) => { + unblock = resolve; + }); + let notify!: () => void; + const entered = new Promise((resolve) => { + notify = resolve; + }); + try { + const call = await f.driver.acceptToolRequest(f.request); + let afterLeaseFences = 0; + f.beforeFence(async () => { + if (f.leases > 0 && ++afterLeaseFences === 2) { + notify(); + await blocked; + } + }); + const pending = f.driver.driveOnce(call.id); + await entered; + const dispatched = (await f.journal.get(call.id))!; + await f.journal.cancel(call.id, f.actorId, new Date(), dispatched.version); + let hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + let offset = 0; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + now: () => new Date(Date.now() + offset), + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + offset = 310_000; + f.advanceNow(offset); + await service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId); + unblock(); + const outcome = await pending; + expect(outcome.state).toBe("outcome_unknown"); + expect((await f.command("stats")).calls).toBe(0); + } finally { + unblock(); + await f.cleanup(); + } +}, 20000); + +test("a different-scope barrier commits before the waiting rebind reads its drain", async () => { + const { and, eq, sql } = await import("drizzle-orm"); + const { mcpExecution } = await import("../db/schema.ts"); + const { reviewContext } = await import("../mcp/journal-review.ts"); + const f = await effectFixture({ classification: "write" }); + let release!: () => void; + const held = new Promise((resolve) => { + release = resolve; + }); + let locked!: () => void; + const ready = new Promise((resolve) => { + locked = resolve; + }); + let blocker: Promise | undefined; + try { + const call = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(call.id); + const approval = await f.journal.requestApproval(call.id, f.deploymentId, new Date()); + const approved = ( + await f.journal.decide(call.id, f.actorId, true, reviewContext(call, approval), new Date()) + ).call; + await f.journal.admit(call.id, f.deploymentId, new Date()); + const lease = (await f.journal.getAdmission(call.id))!; + let hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + let applied = 0; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + applied++; + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: crypto.randomUUID(), + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + blocker = f.db.transaction(async (tx) => { + await tx.select().from(mcpExecution).where(eq(mcpExecution.id, call.id)).for("update"); + locked(); + await held; + }); + await ready; + const barrier = f.journal.dispatch( + call.id, + { bootId: "boot", intentId: crypto.randomUUID(), expiresAt: lease.expiresAt }, + new Date(), + { + executionId: call.id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: approved.version, + reservationId: lease.reservationId, + reservationExpiresAt: lease.expiresAt, + }, + ); + async function waitForLocks(minimum: number) { + const deadline = Date.now() + 3000; + while (Date.now() < deadline) { + const result = await f.db.execute( + sql`select count(*)::integer as n from pg_stat_activity where datname = current_database() and wait_event_type = 'Lock' and state = 'active'`, + ); + if (Number(result.rows[0]?.n) >= minimum) return; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + throw Error("expected lock wait unavailable"); + } + await waitForLocks(1); + const commit = service + .commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId) + .then( + () => "committed", + (error: Error) => error.message, + ); + await waitForLocks(2); + release(); + await blocker; + expect((await barrier).state).toBe("dispatching"); + expect(await commit).not.toBe("committed"); + expect(applied).toBe(0); + } finally { + release(); + await blocker; + await f.cleanup(); + } +}, 20000); + +test("an accepted old call cannot cross the barrier after rebind", async () => { + const f = await effectFixture({ classification: "read" }); + try { + const call = await f.driver.acceptToolRequest(f.request); + let hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + await service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId); + await f.driver.driveOnce(call.id).catch(() => undefined); + expect((await f.command("stats")).calls).toBe(0); + expect((await f.journal.get(call.id))!.state).not.toBe("dispatching"); + } finally { + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-rebind.test.ts b/orchestrator/src/__tests__/mcp-rebind.test.ts new file mode 100644 index 000000000..101b8f574 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-rebind.test.ts @@ -0,0 +1,863 @@ +import { expect, test } from "bun:test"; +import { eq } from "drizzle-orm"; +import { taskSession } from "../db/schema.ts"; +import { effectFixture } from "./mcp-effect-fixture.ts"; +import { makeMcpRebind } from "../mcp/rebind.ts"; + +test("explicit reviewed rebind installs exact deny acknowledgement before atomic exposure", async () => { + const f = await effectFixture({ classification: "read" }); + try { + let hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + expect(previous).toEqual(hostState); + const [row] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + expect(row!.mcpBindingEnvelope!.bindingHash).toBe(f.snapshot.bindingHash); + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + const result = await service.commit( + f.sessionId, + f.snapshot.bindingHash, + candidate.bindingHash, + f.actorId, + ); + expect(result.bindingHash).toBe(candidate.bindingHash); + const [row] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + expect(row!.mcpBindingEnvelope!.bindingHash).toBe(candidate.bindingHash); + expect(hostState.bindingHash).toBe(candidate.bindingHash); + } finally { + await f.cleanup(); + } +}, 20000); + +test("typed rebind requires the exact reviewed candidate", async () => { + const { createClient } = await import("@connectrpc/connect"); + const { createConnectTransport } = await import("@connectrpc/connect-node"); + const { Hono } = await import("hono"); + const { buildServer } = await import("../server.ts"); + const { IntegrationService } = await import("../gen/engram/app/v1/integration_pb.ts"); + const { McpRebindDrainReason, McpRebindDrainSchema } = + await import("../gen/engram/app/v1/integration_pb.ts"); + const { McpRebindDraining } = await import("../mcp/rebind.ts"); + const { ConnectError } = await import("@connectrpc/connect"); + const { mcpHandlers } = await import("../mcp/contracts.ts"); + const f = await effectFixture({ classification: "read" }); + try { + let hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + expect(previous).toEqual(hostState); + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const server = buildServer(new Hono(), (router) => + router.service( + IntegrationService, + mcpHandlers({ + services: { + enabled: true, + journal: f.journal, + repository: f.repository, + runtime: { + cancel: async (id) => f.driver.cancel(id), + prepareRebind: service.prepare, + rebind: async (session, old, candidate, actor) => { + if (candidate === "drain") + throw new McpRebindDraining({ + reason: "unknown_intent", + retryAfter: "2030-01-01T00:00:00.000Z", + }); + return service.commit(session, old, candidate, actor); + }, + }, + }, + connections: f.connections, + getSession: async () => ({ + user: { id: f.actorId, role: "user", email: "user@example.test" }, + }), + now: () => new Date(), + connectionToProto: (row) => ({ id: row.id }), + }), + ), + ); + const url = await new Promise((resolve) => + server.listen(0, "127.0.0.1", () => + resolve( + `http://127.0.0.1:${(server.address() as import("node:net").AddressInfo).port}/rpc`, + ), + ), + ); + const client = createClient( + IntegrationService, + createConnectTransport({ baseUrl: url, httpVersion: "1.1" }), + ); + try { + const candidate = await client.prepareMcpRebind({ + sessionId: f.sessionId, + expectedBindingHash: f.snapshot.bindingHash, + }); + expect(candidate.bindingHash).toBeTruthy(); + expect(candidate.bindingTools.length).toBeGreaterThan(0); + try { + await client.rebindMcp({ + sessionId: f.sessionId, + expectedBindingHash: f.snapshot.bindingHash, + expectedCandidateHash: "drain", + }); + throw Error("expected drain"); + } catch (error) { + expect(error).toBeInstanceOf(ConnectError); + const detail = (error as InstanceType).findDetails( + McpRebindDrainSchema, + )[0]; + expect(detail?.reason).toBe(McpRebindDrainReason.UNKNOWN_INTENT); + expect(detail?.retryAfter).toBe("2030-01-01T00:00:00.000Z"); + } + await expect( + client.rebindMcp({ + sessionId: f.sessionId, + expectedBindingHash: f.snapshot.bindingHash, + expectedCandidateHash: "changed", + }), + ).rejects.toThrow(); + const committed = await client.rebindMcp({ + sessionId: f.sessionId, + expectedBindingHash: f.snapshot.bindingHash, + expectedCandidateHash: candidate.bindingHash, + }); + expect(committed.bindingHash).toBe(candidate.bindingHash); + } finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } + } finally { + await f.cleanup(); + } +}, 20000); + +test("lost host acknowledgement leaves old snapshot and preserves installed denies on retry", async () => { + const f = await effectFixture({ classification: "read" }); + try { + let hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + let lose = true; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + expect(previous).toEqual(hostState); + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + if (lose) { + lose = false; + throw Error("lost acknowledgement"); + } + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const first = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + await expect( + service.commit(f.sessionId, f.snapshot.bindingHash, first.bindingHash, f.actorId), + ).rejects.toThrow("lost acknowledgement"); + const [unchanged] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + expect(unchanged!.mcpBindingEnvelope!.bindingHash).toBe(f.snapshot.bindingHash); + const retry = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + expect(retry.managedDenyPolicy.revision).toBeGreaterThan(first.managedDenyPolicy.revision); + expect( + first.managedDenyPolicy.hosts.every((name) => retry.managedDenyPolicy.hosts.includes(name)), + ).toBe(true); + await service.commit(f.sessionId, f.snapshot.bindingHash, retry.bindingHash, f.actorId); + expect(hostState.bindingHash).toBe(retry.bindingHash); + } finally { + await f.cleanup(); + } +}, 20000); + +test("stale hashes, lost owner and changed connection cannot apply a reviewed candidate", async () => { + const { user, integrationConnection } = await import("../db/schema.ts"); + const f = await effectFixture({ classification: "read" }); + try { + const hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + let applied = 0; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + ) { + applied++; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + await expect( + service.commit(f.sessionId, "stale", candidate.bindingHash, f.actorId), + ).rejects.toThrow(); + await expect( + service.commit(f.sessionId, f.snapshot.bindingHash, "stale", f.actorId), + ).rejects.toThrow(); + await f.db.update(user).set({ banned: true }).where(eq(user.id, f.actorId)); + await expect( + service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId), + ).rejects.toThrow(); + await f.db.update(user).set({ banned: false }).where(eq(user.id, f.actorId)); + await f.db + .update(integrationConnection) + .set({ alias: "changed" }) + .where(eq(integrationConnection.id, f.connection.id)); + await expect( + service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId), + ).rejects.toThrow(); + expect(applied).toBe(0); + const [row] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + expect(row!.mcpBindingEnvelope!.bindingHash).toBe(f.snapshot.bindingHash); + } finally { + await f.cleanup(); + } +}, 20000); + +test("concurrent commits expose only one reviewed binding", async () => { + const f = await effectFixture({ classification: "read" }); + try { + let hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + let applied = 0; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + expect(previous).toEqual(hostState); + applied++; + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + const results = await Promise.allSettled([ + service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId), + service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId), + ]); + expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); + expect(applied).toBe(1); + const [row] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + expect(row!.mcpBindingEnvelope!.bindingHash).toBe(candidate.bindingHash); + } finally { + await f.cleanup(); + } +}, 20000); + +test("physical placement change after host acknowledgement prevents exposure", async () => { + const f = await effectFixture({ classification: "read" }); + try { + let epoch = f.snapshot.bindingEpoch; + const hostState = { + sessionId: f.sessionId, + bindingEpoch: epoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + ) { + epoch++; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => epoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + await expect( + service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId), + ).rejects.toThrow("placement changed"); + const [row] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + expect(row!.mcpBindingEnvelope!.bindingHash).toBe(f.snapshot.bindingHash); + } finally { + await f.cleanup(); + } +}, 20000); + +test("changed grant publication rejects commit before host apply", async () => { + const { profile } = await import("../db/schema.ts"); + const f = await effectFixture({ classification: "read" }); + try { + const hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + let applied = 0; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + ) { + applied++; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + await f.db + .update(profile) + .set({ mcpGrantPublication: null }) + .where(eq(profile.id, f.profileId)); + await expect( + service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId), + ).rejects.toThrow("publication"); + expect(applied).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); + +test("child rebind retains the exact current parent intersection", async () => { + const { task } = await import("../db/schema.ts"); + const { readLiveBindingAuthority, bindingSnapshotColumns } = + await import("../mcp/binding-store.ts"); + const { compileSessionBindings } = await import("../mcp/session-bindings.ts"); + const f = await effectFixture({ classification: "write" }); + const childTask = crypto.randomUUID(), + childId = crypto.randomUUID(); + try { + const live = await readLiveBindingAuthority(f.profileId, f.operatorPolicies, f.db); + expect(live.profileGrants.length).toBeGreaterThan(1); + const firstGrant = live.profileGrants[0]!; + const parentGrant = { + ...firstGrant, + mcp: { + ...firstGrant.mcp!, + unrestricted: false, + automationWrite: false, + argumentConstraintDocuments: [{ type: "object", properties: { x: { maximum: 5 } } }], + }, + }; + const parent = compileSessionBindings({ + launchPolicy: { profileId: f.profileId, integrationGrants: live.profileGrants }, + liveAuthority: live, + bindingEpoch: f.snapshot.bindingEpoch, + requestedGrants: [parentGrant], + }); + const [original] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + const parentColumns = bindingSnapshotColumns(parent, original!.integrationConnections ?? []); + await f.db.update(taskSession).set(parentColumns).where(eq(taskSession.sessionId, f.sessionId)); + const child = compileSessionBindings({ + launchPolicy: { profileId: f.profileId, integrationGrants: live.profileGrants }, + liveAuthority: live, + bindingEpoch: 0, + requestedGrants: [live.profileGrants[0]!], + parentAuthority: { sessionId: f.sessionId, snapshot: parent }, + }); + await f.db.insert(task).values({ + id: childTask, + type: "chat", + createdByUserId: f.actorId, + spawningSessionId: f.sessionId, + }); + await f.db.insert(taskSession).values({ + taskId: childTask, + sessionId: childId, + profileId: f.profileId, + integrationPrincipalId: f.actorId, + ...bindingSnapshotColumns(child, original!.integrationConnections ?? []), + }); + let hostState = { + sessionId: childId, + bindingEpoch: 0, + bindingHash: child.bindingHash, + policy: child.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + let parentCurrent = true; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => 0, + readParentCurrent: async (id) => { + if (!parentCurrent) throw Error("parent stale"); + return f.resolver.readCurrent(id); + }, + }); + const candidate = await service.prepare(childId, child.bindingHash, f.actorId); + expect(candidate.ancestorAuthorityRefs[0]!.bindingHash).toBe(parent.bindingHash); + expect(candidate.immutableBindings.flatMap((binding) => binding.tools)).toHaveLength(1); + const effective = candidate.immutableBindings[0]!.tools[0]!.grant; + expect(effective.automationWrite).toBe(false); + expect(effective.unrestricted).toBe(false); + expect(effective.argumentConstraintDocuments).toContainEqual({ + type: "object", + properties: { x: { maximum: 5 } }, + }); + expect(candidate.profileGrantsHash).toBe(parent.profileGrantsHash); + parentCurrent = false; + await expect( + service.commit(childId, child.bindingHash, candidate.bindingHash, f.actorId), + ).rejects.toThrow("parent stale"); + parentCurrent = true; + await service.commit(childId, child.bindingHash, candidate.bindingHash, f.actorId); + expect(hostState.bindingHash).toBe(candidate.bindingHash); + } finally { + await f.db.delete(taskSession).where(eq(taskSession.sessionId, childId)); + await f.db.delete(task).where(eq(task.id, childTask)); + await f.cleanup(); + } +}, 20000); + +test("new published leaf grants expand only through the exact reviewed rebind", async () => { + const { readLiveBindingAuthority, bindingSnapshotColumns } = + await import("../mcp/binding-store.ts"); + const { compileSessionBindings } = await import("../mcp/session-bindings.ts"); + const { profile } = await import("../db/schema.ts"); + const f = await effectFixture({ classification: "write" }); + try { + const live = await readLiveBindingAuthority(f.profileId, f.operatorPolicies, f.db); + const old = compileSessionBindings({ + launchPolicy: { profileId: f.profileId, integrationGrants: live.profileGrants }, + liveAuthority: live, + bindingEpoch: f.snapshot.bindingEpoch, + requestedGrants: [live.profileGrants[0]!], + }); + const [row] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + await f.db + .update(taskSession) + .set(bindingSnapshotColumns(old, row!.integrationConnections ?? [])) + .where(eq(taskSession.sessionId, f.sessionId)); + let hostState = { + sessionId: f.sessionId, + bindingEpoch: old.bindingEpoch, + bindingHash: old.bindingHash, + policy: old.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => old.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, old.bindingHash, f.actorId); + expect(old.immutableBindings.flatMap((binding) => binding.tools)).toHaveLength(1); + expect(candidate.immutableBindings.flatMap((binding) => binding.tools)).toHaveLength(2); + await expect( + service.commit(f.sessionId, old.bindingHash, "unreviewed", f.actorId), + ).rejects.toThrow(); + expect(hostState.bindingHash).toBe(old.bindingHash); + await service.commit(f.sessionId, old.bindingHash, candidate.bindingHash, f.actorId); + expect(hostState.bindingHash).toBe(candidate.bindingHash); + await f.db + .update(profile) + .set({ mcpGrantPublication: null }) + .where(eq(profile.id, f.profileId)); + await expect(service.prepare(f.sessionId, candidate.bindingHash, f.actorId)).rejects.toThrow( + "publication", + ); + } finally { + await f.cleanup(); + } +}, 20000); + +test("a newly administrator-published leaf grant is adopted only after candidate review", async () => { + const { user } = await import("../db/schema.ts"); + const { makeProfileStore } = await import("../db/profiles.ts"); + const { readLiveBindingAuthority, bindingSnapshotColumns } = + await import("../mcp/binding-store.ts"); + const { compileSessionBindings } = await import("../mcp/session-bindings.ts"); + const f = await effectFixture({ classification: "write" }); + try { + await f.db.update(user).set({ role: "admin" }).where(eq(user.id, f.actorId)); + const store = makeProfileStore(f.db); + const full = (await store.get(f.profileId))!; + const grants = full.integrationGrants; + expect(grants.length).toBeGreaterThan(1); + await store.update( + f.profileId, + { ...full, integrationGrants: [grants[0]!] }, + { actorId: f.actorId, expectedRevision: 1 }, + ); + const narrowLive = await readLiveBindingAuthority(f.profileId, f.operatorPolicies, f.db); + const old = compileSessionBindings({ + launchPolicy: { profileId: f.profileId, integrationGrants: narrowLive.profileGrants }, + liveAuthority: narrowLive, + bindingEpoch: f.snapshot.bindingEpoch, + }); + const [row] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + await f.db + .update(taskSession) + .set(bindingSnapshotColumns(old, row!.integrationConnections ?? [])) + .where(eq(taskSession.sessionId, f.sessionId)); + const narrowProfile = (await store.get(f.profileId))!; + await store.update( + f.profileId, + { ...narrowProfile, integrationGrants: grants }, + { actorId: f.actorId, expectedRevision: 2 }, + ); + let hostState = { + sessionId: f.sessionId, + bindingEpoch: old.bindingEpoch, + bindingHash: old.bindingHash, + policy: old.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof hostState.policy, + ) { + hostState = { ...hostState, bindingHash: expected.bindingHash, policy }; + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => old.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, old.bindingHash, f.actorId); + expect(candidate.profilePublication!.revision).toBe(3); + expect(candidate.immutableBindings.flatMap((binding) => binding.tools)).toHaveLength(2); + await expect( + service.commit(f.sessionId, old.bindingHash, "wrong", f.actorId), + ).rejects.toThrow(); + expect(hostState.bindingHash).toBe(old.bindingHash); + await service.commit(f.sessionId, old.bindingHash, candidate.bindingHash, f.actorId); + expect(hostState.bindingHash).toBe(candidate.bindingHash); + } finally { + await f.cleanup(); + } +}, 20000); + +test("rebind remains closed when the trusted host producer is absent", async () => { + const f = await effectFixture({ classification: "read" }); + try { + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + await expect(service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId)).rejects.toThrow( + "unavailable", + ); + await expect( + service.commit(f.sessionId, f.snapshot.bindingHash, "candidate", f.actorId), + ).rejects.toThrow("unavailable"); + } finally { + await f.cleanup(); + } +}, 20000); + +test("managed authority inserted during host acknowledgement prevents exposure", async () => { + const { integrationConnection } = await import("../db/schema.ts"); + const f = await effectFixture({ classification: "read" }); + const insertedId = crypto.randomUUID(); + try { + const hostState = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return structuredClone(hostState); + }, + async compareAndApply( + _previous: typeof hostState, + expected: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + ) { + const [source] = await f.db + .select() + .from(integrationConnection) + .where(eq(integrationConnection.id, f.connection.id)); + await f.db + .insert(integrationConnection) + .values({ ...source!, id: insertedId, alias: `new-managed-${insertedId}` }); + return expected; + }, + }; + const service = makeMcpRebind({ + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + await expect( + service.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId), + ).rejects.toThrow("managed authority changed"); + const [row] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + expect(row!.mcpBindingEnvelope!.bindingHash).toBe(f.snapshot.bindingHash); + } finally { + await f.db.delete(integrationConnection).where(eq(integrationConnection.id, insertedId)); + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-review-store.test.ts b/orchestrator/src/__tests__/mcp-review-store.test.ts index b67ab8b41..19065b534 100644 --- a/orchestrator/src/__tests__/mcp-review-store.test.ts +++ b/orchestrator/src/__tests__/mcp-review-store.test.ts @@ -1,3 +1,4 @@ +import { unknownContext } from "../mcp/journal.ts"; import { expect, test } from "bun:test"; import { eq } from "drizzle-orm"; import { checkDb, getDb } from "../db/client.ts"; @@ -304,7 +305,13 @@ test.skipIf(!reachable)( const page = await f.journal.readSession(f.sessionId, f.actorId, { after: "zzzz" }); expect(page.rows).toHaveLength(0); expect(page.unknownWriteBlocks).toEqual([f.connection.id]); - await f.journal.resolveUnknown(call.id, f.actorId, "acknowledged", f.now); + await f.journal.resolveUnknown( + call.id, + f.actorId, + "acknowledged", + f.now, + unknownContext((await f.journal.get(call.id))!), + ); expect((await f.journal.readSession(f.sessionId, f.actorId)).unknownWriteBlocks).toHaveLength( 0, ); diff --git a/orchestrator/src/__tests__/mcp-rpc.test.ts b/orchestrator/src/__tests__/mcp-rpc.test.ts index de7ba1fe4..ffccc5e1f 100644 --- a/orchestrator/src/__tests__/mcp-rpc.test.ts +++ b/orchestrator/src/__tests__/mcp-rpc.test.ts @@ -81,6 +81,8 @@ function services(): McpServices { dispatch: unavailable, finish: unavailable, cancel: unavailable, + cancelInternal: unavailable, + cancellationReceipt: unavailable, resolveUnknown: unavailable, expire: unavailable, expireUnadmitted: unavailable, diff --git a/orchestrator/src/__tests__/mcp-runtime-authority.test.ts b/orchestrator/src/__tests__/mcp-runtime-authority.test.ts index ad2990444..b0c56ba4d 100644 --- a/orchestrator/src/__tests__/mcp-runtime-authority.test.ts +++ b/orchestrator/src/__tests__/mcp-runtime-authority.test.ts @@ -226,7 +226,7 @@ test("barrier and human cancellation share a safe lock order", async () => { try { const results = await Promise.allSettled([ f.journal.dispatch(f.call.id, f.intent, f.now, f.proof), - f.journal.cancel(f.call.id, f.actorId, f.now), + f.journal.cancel(f.call.id, f.actorId, f.now, (await f.journal.get(f.call.id))!.version), ]); expect(results[1]!.status).toBe("fulfilled"); expect(["canceled", "outcome_unknown"]).toContain((await f.journal.get(f.call.id))!.state); diff --git a/orchestrator/src/__tests__/mcp-store.test.ts b/orchestrator/src/__tests__/mcp-store.test.ts index 26fc371d4..d4e436601 100644 --- a/orchestrator/src/__tests__/mcp-store.test.ts +++ b/orchestrator/src/__tests__/mcp-store.test.ts @@ -1,3 +1,4 @@ +import { unknownContext } from "../mcp/journal.ts"; import { describe, expect, test } from "bun:test"; import { eq, sql } from "drizzle-orm"; import { checkDb } from "../db/client.ts"; @@ -82,15 +83,15 @@ async function fixture(deploymentId?: string) { reservationExpiresAt: (await base.getAdmission(id))!.expiresAt, }); }, - cancel: (id: string, who: string, _cachedAdmin: boolean, now: Date) => - base.cancel(id, actor(who), now), - resolveUnknown: ( + cancel: async (id: string, who: string, _cachedAdmin: boolean, now: Date) => + base.cancel(id, actor(who), now, (await base.get(id))!.version), + resolveUnknown: async ( id: string, who: string, _cachedAdmin: boolean, outcome: "succeeded" | "failed" | "acknowledged", now: Date, - ) => base.resolveUnknown(id, actor(who), outcome, now), + ) => base.resolveUnknown(id, actor(who), outcome, now, unknownContext((await base.get(id))!)), }; return { ...f, input, journal, row: f.connection, repo: f.repository, published: f.catalog }; } diff --git a/orchestrator/src/__tests__/mcp-unknown-drain.test.ts b/orchestrator/src/__tests__/mcp-unknown-drain.test.ts new file mode 100644 index 000000000..9330883cd --- /dev/null +++ b/orchestrator/src/__tests__/mcp-unknown-drain.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from "bun:test"; +import { reviewContext } from "../mcp/journal-review.ts"; +import { effectFixture } from "./mcp-effect-fixture.ts"; + +test("manual resolution cannot authorize a new write while the old intent can send", async () => { + const f = await effectFixture({ classification: "write" }); + let unblock!: () => void; + const blocked = new Promise((resolve) => { + unblock = resolve; + }); + let notify!: () => void; + const entered = new Promise((resolve) => { + notify = resolve; + }); + try { + const first = await f.driver.acceptToolRequest(f.request); + await f.driver.driveOnce(first.id); + const approval = await f.journal.requestApproval(first.id, f.deploymentId, new Date()); + await f.journal.decide(first.id, f.actorId, true, reviewContext(first, approval), new Date()); + let fences = 0; + f.beforeFence(async () => { + if (f.leases > 0 && ++fences === 2) { + notify(); + await blocked; + } + }); + const pending = f.driver.driveOnce(first.id); + await entered; + const earlierRequest = { ...f.request, toolCallId: "already-accepted" }; + f.requests.set(earlierRequest.toolCallId, earlierRequest); + const earlier = await f.driver.acceptToolRequest(earlierRequest); + const dispatched = (await f.journal.get(first.id))!; + const unknown = await f.journal.cancel(first.id, f.actorId, new Date(), dispatched.version); + const context = { + executionId: first.id, + journalRevision: unknown.version, + requestDigest: unknown.requestDigest, + bindingHash: unknown.bindingHash, + intentId: unknown.intentId!, + }; + await f.journal.resolveUnknown(first.id, f.actorId, "acknowledged", new Date(), context); + const earlierAttempt = await f.driver.driveOnce(earlier.id).then( + () => "accepted", + () => "blocked", + ); + const next = { ...f.request, toolCallId: "new-call" }; + f.requests.set(next.toolCallId, next); + const attempted = await f.driver.acceptToolRequest(next).then( + () => "accepted", + () => "blocked", + ); + f.advanceNow(310_000); + unblock(); + await pending; + expect(attempted).toBe("blocked"); + expect(earlierAttempt).toBe("blocked"); + expect((await f.command("stats")).calls).toBe(0); + const accepted = await f.driver.acceptToolRequest(next); + expect(accepted.toolCallId).toBe("new-call"); + } finally { + unblock(); + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/control-plane/transport.ts b/orchestrator/src/control-plane/transport.ts index 7b505cde2..5c28dcd67 100644 --- a/orchestrator/src/control-plane/transport.ts +++ b/orchestrator/src/control-plane/transport.ts @@ -21,6 +21,7 @@ import { config } from "../config.ts"; import { makeLeaseStore } from "../listeners/lease-store.ts"; import { makeListenerWakeInterceptor } from "../listeners/wake-interceptor.ts"; import { log } from "../log.ts"; +import { makeMcpLifecycleInterceptor } from "../mcp/lifecycle-interceptor.ts"; // Machine identity (ADR §5): one trusted caller, one static credential. export const bearerInterceptor: Interceptor = (next) => (req) => { @@ -67,6 +68,19 @@ export const controlPlaneTransport: Transport = makeTransport( config.controlPlaneGrpcUrl, config.controlPlaneBearer, [ + makeMcpLifecycleInterceptor(async (sessionId, cause) => { + const [{ getDb }, { makeMcpLifecycleInvalidator }, { mcpRecovery }] = await Promise.all([ + import("../db/client.ts"), + import("../mcp/lifecycle-invalidation.ts"), + import("../mcp/recovery.ts"), + ]); + await makeMcpLifecycleInvalidator({ + db: getDb(), + abort: async (id) => { + mcpRecovery()?.abortExecution(id); + }, + })(sessionId, cause); + }), // Every resuming session RPC wakes the session's listener row (ADR 0119 // amendment): this is the one seam all callers share. makeListenerWakeInterceptor( diff --git a/orchestrator/src/db/integration-connections.ts b/orchestrator/src/db/integration-connections.ts index 0e0abcfc3..04dd00ea5 100644 --- a/orchestrator/src/db/integration-connections.ts +++ b/orchestrator/src/db/integration-connections.ts @@ -1,3 +1,4 @@ +import { notifyMcpAuthorityChanged } from "../mcp/authority-notification.ts"; /** Named integration connection store (ADR 0109). */ import { validateMcpConfig } from "../mcp/config.ts"; @@ -42,7 +43,10 @@ export interface IntegrationConnectionStore { getMany(ids: readonly string[]): Promise; getDefault(provider: string): Promise; create(input: IntegrationConnectionInput): Promise; - update(id: string, input: Omit): Promise; + update( + id: string, + input: Omit, + ): Promise; delete(id: string): Promise; markTested(id: string, testedAt: Date): Promise; setEnabled(id: string, enabled: boolean): Promise; @@ -55,15 +59,17 @@ export interface IntegrationConnectionStore { function toRow(r: typeof connectionTable.$inferSelect): IntegrationConnectionRow { return { - ...(r.provider === "mcp" ? { - mcp: { - configRevision: r.configRevision, - authGeneration: r.authGeneration, - credentialIdentity: r.credentialIdentity, - catalogRevision: r.catalogRevision, - revoked: r.revoked, - }, - } : {}), + ...(r.provider === "mcp" + ? { + mcp: { + configRevision: r.configRevision, + authGeneration: r.authGeneration, + credentialIdentity: r.credentialIdentity, + catalogRevision: r.catalogRevision, + revoked: r.revoked, + }, + } + : {}), id: r.id, alias: r.alias, provider: r.provider, @@ -84,17 +90,16 @@ export function makeIntegrationConnectionStore( const rows = await db .select() .from(connectionTable) - .where(and( - eq(connectionTable.provider, provider), - eq(connectionTable.isDefault, true), - )) + .where(and(eq(connectionTable.provider, provider), eq(connectionTable.isDefault, true))) .limit(1); return rows[0] ? toRow(rows[0]) : null; } return { async list() { - return (await db.select().from(connectionTable).orderBy(asc(connectionTable.alias))).map(toRow); + return (await db.select().from(connectionTable).orderBy(asc(connectionTable.alias))).map( + toRow, + ); }, async get(id) { @@ -123,6 +128,7 @@ export function makeIntegrationConnectionStore( .insert(connectionTable) .values({ id: crypto.randomUUID(), ...input, enabled: false }) .returning(); + if (input.provider === "mcp") notifyMcpAuthorityChanged(); return toRow(rows[0]!); }, @@ -139,7 +145,8 @@ export function makeIntegrationConnectionStore( const rows = await db .delete(connectionTable) .where(eq(connectionTable.id, id)) - .returning({ id: connectionTable.id }); + .returning({ id: connectionTable.id, provider: connectionTable.provider }); + if (rows[0]?.provider === "mcp") notifyMcpAuthorityChanged(); return rows.length > 0; }, @@ -174,16 +181,19 @@ export function makeIntegrationConnectionStore( if (provider === "mcp") throw new Error("MCP requires an explicit named connection"); const existing = await getDefault(provider); if (existing) return existing; - await db.insert(connectionTable).values({ - id: crypto.randomUUID(), - alias: `${provider}-default`, - provider, - displayName, - isDefault: true, - config: {}, - enabled: true, - testedAt: new Date(0), - }).onConflictDoNothing(); + await db + .insert(connectionTable) + .values({ + id: crypto.randomUUID(), + alias: `${provider}-default`, + provider, + displayName, + isDefault: true, + config: {}, + enabled: true, + testedAt: new Date(0), + }) + .onConflictDoNothing(); const created = await getDefault(provider); if (!created) { throw new Error(`failed to create default integration connection for "${provider}"`); diff --git a/orchestrator/src/db/profiles.ts b/orchestrator/src/db/profiles.ts index f3ed7f553..d13eb6bce 100644 --- a/orchestrator/src/db/profiles.ts +++ b/orchestrator/src/db/profiles.ts @@ -8,6 +8,7 @@ import { and, eq, inArray, isNull, ne } from "drizzle-orm"; +import { notifyMcpAuthorityChanged } from "../mcp/authority-notification.ts"; import { canonicalDigest } from "../mcp/hash.ts"; import { isServiceAccountEmail } from "../rpc/api-key.ts"; import { getDb } from "./client.ts"; @@ -163,7 +164,7 @@ export function makeProfileStore(db: ReturnType = getDb()): Profil }); }, async update(id, input, publication) { - return db.transaction(async (tx) => { + const result = await db.transaction(async (tx) => { await lockPublisher(tx, publication); const [existing] = await tx .select() @@ -224,12 +225,15 @@ export function makeProfileStore(db: ReturnType = getDb()): Profil .returning(); return toRow(row!); }); + if (result) notifyMcpAuthorityChanged(); + return result; }, async softDelete(id) { await db .update(profileTable) .set({ deletedAt: new Date() }) .where(and(eq(profileTable.id, id), isNull(profileTable.deletedAt))); + notifyMcpAuthorityChanged(); }, }; } diff --git a/orchestrator/src/db/schema.ts b/orchestrator/src/db/schema.ts index e74a43f0f..6133fe005 100644 --- a/orchestrator/src/db/schema.ts +++ b/orchestrator/src/db/schema.ts @@ -2265,6 +2265,9 @@ export const mcpExecution = pgTable( index("mcp_execution_connection_state_idx").on(t.connectionId, t.sessionId, t.state), index("mcp_execution_catalog_idx").on(t.catalogId), index("mcp_execution_pending_idx").on(t.state, t.deadline), + index("mcp_execution_lifecycle_active_idx") + .on(t.id) + .where(sql`${t.state} IN ('waiting_approval','authorized','dispatching')`), index("mcp_execution_recovery_idx") .on(t.id) .where(sql`${t.completionEnqueuedVersion} < ${t.version}`), @@ -2318,12 +2321,37 @@ export const mcpApproval = pgTable( ], ); +/** A cancellation is a durable decision, not proof that an upstream effect was undone. */ +export const mcpCancellation = pgTable( + "mcp_cancellation", + { + executionId: text("execution_id") + .primaryKey() + .references(() => mcpExecution.id, { onDelete: "cascade" }), + actorId: text("actor_id"), + cause: text("cause").notNull(), + expectedJournalRevision: integer("expected_journal_revision").notNull(), + journalRevision: integer("journal_revision").notNull(), + state: text("state").$type<"canceled" | "outcome_unknown">().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull(), + }, + (t) => [ + check( + "mcp_cancellation_revision_check", + sql`${t.expectedJournalRevision} > 0 AND ${t.journalRevision} = ${t.expectedJournalRevision} + 1`, + ), + check("mcp_cancellation_state_check", sql`${t.state} IN ('canceled','outcome_unknown')`), + ], +); + export const mcpResolution = pgTable( "mcp_resolution", { executionId: text("execution_id") .primaryKey() .references(() => mcpExecution.id, { onDelete: "cascade" }), + expectedContext: + jsonb("expected_context").$type(), actorId: text("actor_id").notNull(), outcome: text("outcome").$type<"succeeded" | "failed" | "acknowledged">().notNull(), createdAt: timestamp("created_at", { withTimezone: true }).notNull(), diff --git a/orchestrator/src/gen/engram/app/v1/integration_pb.ts b/orchestrator/src/gen/engram/app/v1/integration_pb.ts index d0c59bd2b..3b03a81cb 100644 --- a/orchestrator/src/gen/engram/app/v1/integration_pb.ts +++ b/orchestrator/src/gen/engram/app/v1/integration_pb.ts @@ -2,8 +2,8 @@ // @generated from file engram/app/v1/integration.proto (package engram.app.v1, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; -import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; import type { GetMcpWritePolicyResponse } from "./session_pb"; import { file_engram_app_v1_session } from "./session_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/integration.proto. */ export const file_engram_app_v1_integration: GenFile = /*@__PURE__*/ - fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIo8DCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkSGgoSb3JpZ2luYWxfYXJndW1lbnRzGAogASgJEi0KDXJldmlld2VkX3Rvb2wYCyABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgMIAMoCRJIChdleHBlY3RlZF9yZXZpZXdfY29udGV4dBgNIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IkkKF0xpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSGgoSYWZ0ZXJfZXhlY3V0aW9uX2lkGAIgASgJImQKGExpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRItCglhcHByb3ZhbHMYASADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhkKEW5leHRfZXhlY3V0aW9uX2lkGAIgASgJIosBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgSSAoXZXhwZWN0ZWRfcmV2aWV3X2NvbnRleHQYAyABKAsyJy5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dCJeChlEZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJEjIKB3JlY2VpcHQYAiABKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmVjZWlwdCIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAkilQEKF1B1dE1jcENyZWRlbnRpYWxSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAiABKA0SGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgDIAEoDRIMCgRraW5kGAQgASgJEg4KBmhlYWRlchgFIAEoCRINCgV2YWx1ZRgGIAEoCSKUAQoTTWNwQ3JlZGVudGlhbFN0YXR1cxIOCgZzdGF0dXMYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJEgwKBGtpbmQYAyABKAkSGwoTY3JlZGVudGlhbF9pZGVudGl0eRgEIAEoCRIPCgd2ZXJzaW9uGAUgASgDEh4KFnJlc2VydmF0aW9uX2dlbmVyYXRpb24YBiABKAMijAEKGFB1dE1jcENyZWRlbnRpYWxSZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyKUAQogQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMikAEKHFJlY292ZXJNY3BDcmVkZW50aWFsUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMihgEKEk1jcENyZWRlbnRpYWxTdGF0ZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyI7ChZNY3BBcHByb3ZlZERlc3RpbmF0aW9uEg8KB3pvbmVfaWQYASABKAkSEAoIZW5kcG9pbnQYAiABKAkiuwEKFU1jcENsaWVudFJlZ2lzdHJhdGlvbhIKCgJpZBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSFAoMY29uZmlkZW50aWFsGAMgASgIEhQKDHJlZGlyZWN0X3VyaRgEIAEoCRIOCgZpc3N1ZXIYBSABKAkSNwoIcmVzb3VyY2UYBiABKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SDgoGc2NvcGVzGAcgAygJIisKEkdldE1jcFNldHVwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJIrwCChNHZXRNY3BTZXR1cFJlc3BvbnNlEg8KB2VuYWJsZWQYASABKAgSOwoNcmVnaXN0cmF0aW9ucxgCIAMoCzIkLmVuZ3JhbS5hcHAudjEuTWNwQ2xpZW50UmVnaXN0cmF0aW9uEjsKDGRlc3RpbmF0aW9ucxgDIAMoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92ZWREZXN0aW5hdGlvbhIyCgdjdXJyZW50GAQgASgLMiEuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdGUSGgoSdHJhbnNpdGlvbl9wZW5kaW5nGAUgASgIEg8KB2Zsb3dfaWQYBiABKAkSFwoPZmxvd19leHBpcmVzX2F0GAcgASgJEiAKGGNsaWVudF9zZWNyZXRfY29uZmlndXJlZBgIIAEoCCLUAQofQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDwoHZmxvd19pZBgEIAEoCRINCgVzdGF0ZRgFIAEoCRIMCgRjb2RlGAYgASgJEhMKBmlzc3VlchgHIAEoCUgAiAEBEhQKDHJlZGlyZWN0X3VyaRgIIAEoCUIJCgdfaXNzdWVyImwKG1JlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0i6gMKGE1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhYKDnJlcXVlc3RfZGlnZXN0GAUgASgJEhUKDWFyZ3VtZW50X2hhc2gYBiABKAkSFAoMYmluZGluZ19oYXNoGAcgASgJEhUKDWNvbm5lY3Rpb25faWQYCCABKAkSEgoKY2F0YWxvZ19pZBgJIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGAogASgNEhcKD2NvbmZpZ19yZXZpc2lvbhgLIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDCABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgNIAEoCRIVCg11cHN0cmVhbV9uYW1lGA4gASgJEhgKEHRvb2xfZmluZ2VycHJpbnQYDyABKAkSFgoOY29udGV4dF9kaWdlc3QYECABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgRIAEoDRISCgpleHBpcmVzX2F0GBIgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgTIAEoCRIVCg1iaW5kaW5nX2Vwb2NoGBQgASgJIr8BChJNY3BBcHByb3ZhbFJlY2VpcHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhAKCGRlY2lzaW9uGAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKCmRlY2lkZWRfYXQYBCABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgFIAEoDRJBChByZXZpZXdlZF9jb250ZXh0GAYgASgLMicuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbFJldmlld0NvbnRleHQi2AMKEk1jcEV4ZWN1dGlvblJldmlldxIUCgxleGVjdXRpb25faWQYASABKAkSFQoNY29ubmVjdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRIYChBqb3VybmFsX3JldmlzaW9uGAQgASgNEhoKEnJlc3VsdF91bmF2YWlsYWJsZRgFIAEoCBIqCgZyZXZpZXcYBiABKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhcKD2FsbG93ZWRfYWN0aW9ucxgHIAMoCRIOCgZyZWFzb24YCCABKAkSFAoMdG9vbF9jYWxsX2lkGAkgASgJEg4KBnJ1bl9pZBgKIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgLIAEoCRIVCg11cHN0cmVhbV9uYW1lGAwgASgJEhkKEWFwcHJvdmFsX2RlY2lzaW9uGA0gASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgOIAEoCRItCg1yZXZpZXdlZF90b29sGA8gASgLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YECADKAkSFAoMYmluZGluZ19oYXNoGBEgASgJImMKFE1jcFJlc29sdXRpb25SZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRIPCgdvdXRjb21lGAMgASgJEhIKCmNyZWF0ZWRfYXQYBCABKAkigQEKEU1jcEJpbmRpbmdSZWNlaXB0EhQKDGJpbmRpbmdfaGFzaBgBIAEoCRIVCg1iaW5kaW5nX2Vwb2NoGAIgASgJEg8KB2N1cnJlbnQYAyABKAgSFQoNc3RhbGVfcmVhc29ucxgEIAMoCRIXCg9hbGxvd2VkX2FjdGlvbnMYBSADKAkigAIKGUdldFNlc3Npb25NY3BTdGF0ZVJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIaChJhZnRlcl9leGVjdXRpb25faWQYAiABKAkSEQoJcGFnZV9zaXplGAMgASgNEh4KEXBsYW5fdG9vbF9jYWxsX2lkGAQgASgJSACIAQESHwoXYWZ0ZXJfYmluZGluZ190b29sX25hbWUYBSABKAkSGQoRYmluZGluZ19wYWdlX3NpemUYBiABKA0SFQoNb21pdF9iaW5kaW5ncxgHIAEoCBIXCg9vbWl0X2V4ZWN1dGlvbnMYCCABKAhCFAoSX3BsYW5fdG9vbF9jYWxsX2lkIr8EChpHZXRTZXNzaW9uTWNwU3RhdGVSZXNwb25zZRI5Cg9iaW5kaW5nX3JlY2VpcHQYASABKAsyIC5lbmdyYW0uYXBwLnYxLk1jcEJpbmRpbmdSZWNlaXB0EjUKCmV4ZWN1dGlvbnMYAiADKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEV4ZWN1dGlvblJldmlldxItCglhcHByb3ZhbHMYAyADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhwKFHVua25vd25fd3JpdGVfYmxvY2tzGAQgAygJEkAKE3Jlc29sdXRpb25fcmVjZWlwdHMYBSADKAsyIy5lbmdyYW0uYXBwLnYxLk1jcFJlc29sdXRpb25SZWNlaXB0EhcKD2FsbG93ZWRfYWN0aW9ucxgGIAMoCRIUCgxhdmFpbGFiaWxpdHkYByABKAkSGQoRbmV4dF9leGVjdXRpb25faWQYCCABKAkSPgoMd3JpdGVfcG9saWN5GAkgASgLMiguZW5ncmFtLmFwcC52MS5HZXRNY3BXcml0ZVBvbGljeVJlc3BvbnNlEjkKDWJpbmRpbmdfdG9vbHMYCiADKAsyIi5lbmdyYW0uYXBwLnYxLk1jcEJvdW5kVG9vbFN1bW1hcnkSHgoWbmV4dF9iaW5kaW5nX3Rvb2xfbmFtZRgLIAEoCRIeChZoYXNfbW9yZV9iaW5kaW5nX3Rvb2xzGAwgASgIEhsKE2hhc19tb3JlX2V4ZWN1dGlvbnMYDSABKAgiygIKE01jcEJvdW5kVG9vbFN1bW1hcnkSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIYChBjb25uZWN0aW9uX2FsaWFzGAIgASgJEhcKD21vZGVsX3Rvb2xfbmFtZRgDIAEoCRItCg1yZXZpZXdlZF90b29sGAQgASgLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YBSADKAkSGAoQYXV0b21hdGlvbl93cml0ZRgGIAEoCBIUCgx1bnJlc3RyaWN0ZWQYByABKAgSEgoKY2F0YWxvZ19pZBgIIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGAkgASgNEhcKD2NvbmZpZ19yZXZpc2lvbhgKIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YCyABKA0y8BYKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USYwoQUHV0TWNwQ3JlZGVudGlhbBImLmVuZ3JhbS5hcHAudjEuUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLlB1dE1jcENyZWRlbnRpYWxSZXNwb25zZRJUCgtHZXRNY3BTZXR1cBIhLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRNY3BTZXR1cFJlc3BvbnNlEnsKGENvbXBsZXRlTWNwQXV0aG9yaXphdGlvbhIuLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBovLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVzcG9uc2USbwoUUmVjb3Zlck1jcENyZWRlbnRpYWwSKi5lbmdyYW0uYXBwLnYxLlJlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuUmVjb3Zlck1jcENyZWRlbnRpYWxSZXNwb25zZRJXCgxBdXRob3JpemVNY3ASIi5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlc3BvbnNlEloKDURpc2Nvbm5lY3RNY3ASIy5lbmdyYW0uYXBwLnYxLkRpc2Nvbm5lY3RNY3BSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVzcG9uc2USaQoSR2V0U2Vzc2lvbk1jcFN0YXRlEiguZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXF1ZXN0GikuZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXNwb25zZRJjChBMaXN0TWNwQXBwcm92YWxzEiYuZW5ncmFtLmFwcC52MS5MaXN0TWNwQXBwcm92YWxzUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1Jlc3BvbnNlEmYKEURlY2lkZU1jcEFwcHJvdmFsEicuZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLkRlY2lkZU1jcEFwcHJvdmFsUmVzcG9uc2USWgoNQ2FuY2VsTWNwQ2FsbBIjLmVuZ3JhbS5hcHAudjEuQ2FuY2VsTWNwQ2FsbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXNwb25zZRJmChFSZXNvbHZlTWNwT3V0Y29tZRInLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5SZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEk4KCVJlYmluZE1jcBIfLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVzcG9uc2ViBnByb3RvMw", [file_engram_app_v1_session]); + fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIo8DCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkSGgoSb3JpZ2luYWxfYXJndW1lbnRzGAogASgJEi0KDXJldmlld2VkX3Rvb2wYCyABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgMIAMoCRJIChdleHBlY3RlZF9yZXZpZXdfY29udGV4dBgNIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IkkKF0xpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSGgoSYWZ0ZXJfZXhlY3V0aW9uX2lkGAIgASgJImQKGExpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRItCglhcHByb3ZhbHMYASADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhkKEW5leHRfZXhlY3V0aW9uX2lkGAIgASgJIosBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgSSAoXZXhwZWN0ZWRfcmV2aWV3X2NvbnRleHQYAyABKAsyJy5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dCJeChlEZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJEjIKB3JlY2VpcHQYAiABKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmVjZWlwdCJPChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSIQoZZXhwZWN0ZWRfam91cm5hbF9yZXZpc2lvbhgCIAEoDSJeChVDYW5jZWxNY3BDYWxsUmVzcG9uc2USDQoFc3RhdGUYASABKAkSNgoHcmVjZWlwdBgCIAEoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQ2FuY2VsbGF0aW9uUmVjZWlwdCKFAQoYUmVzb2x2ZU1jcE91dGNvbWVSZXF1ZXN0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIPCgdvdXRjb21lGAIgASgJEkIKGGV4cGVjdGVkX3Vua25vd25fY29udGV4dBgDIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwVW5rbm93bkNvbnRleHQiYgoZUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRIPCgdvdXRjb21lGAEgASgJEjQKB3JlY2VpcHQYAiABKAsyIy5lbmdyYW0uYXBwLnYxLk1jcFJlc29sdXRpb25SZWNlaXB0IkwKF1ByZXBhcmVNY3BSZWJpbmRSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSHQoVZXhwZWN0ZWRfYmluZGluZ19oYXNoGAIgASgJIqkCChhQcmVwYXJlTWNwUmViaW5kUmVzcG9uc2USFAoMYmluZGluZ19oYXNoGAEgASgJEhUKDWJpbmRpbmdfZXBvY2gYAiABKAkSOQoNYmluZGluZ190b29scxgDIAMoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQm91bmRUb29sU3VtbWFyeRISCgpkZW55X2hvc3RzGAQgAygJEhUKDWRlbnlfcmV2aXNpb24YBSABKAkSEwoLZGVueV9kaWdlc3QYBiABKAkSNwoLY29ubmVjdGlvbnMYByADKAsyIi5lbmdyYW0uYXBwLnYxLk1jcFJlYmluZENvbm5lY3Rpb24SLAoFZHJhaW4YCCABKAsyHS5lbmdyYW0uYXBwLnYxLk1jcFJlYmluZERyYWluIloKDk1jcFJlYmluZERyYWluEjMKBnJlYXNvbhgBIAEoDjIjLmVuZ3JhbS5hcHAudjEuTWNwUmViaW5kRHJhaW5SZWFzb24SEwoLcmV0cnlfYWZ0ZXIYAiABKAkixAEKE01jcFJlYmluZENvbm5lY3Rpb24SFQoNY29ubmVjdGlvbl9pZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRIMCgR6b25lGAQgASgJEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYBSABKAkSFwoPY29uZmlnX3JldmlzaW9uGAYgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgHIAEoDRIYChBjYXRhbG9nX3JldmlzaW9uGAggASgNImYKEFJlYmluZE1jcFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIdChVleHBlY3RlZF9iaW5kaW5nX2hhc2gYAiABKAkSHwoXZXhwZWN0ZWRfY2FuZGlkYXRlX2hhc2gYAyABKAkiKQoRUmViaW5kTWNwUmVzcG9uc2USFAoMYmluZGluZ19oYXNoGAEgASgJIpUBChdQdXRNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDAoEa2luZBgEIAEoCRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkilAEKE01jcENyZWRlbnRpYWxTdGF0dXMSDgoGc3RhdHVzGAEgASgJEhEKCXJlZmVyZW5jZRgCIAEoCRIMCgRraW5kGAMgASgJEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYBCABKAkSDwoHdmVyc2lvbhgFIAEoAxIeChZyZXNlcnZhdGlvbl9nZW5lcmF0aW9uGAYgASgDIowBChhQdXRNY3BDcmVkZW50aWFsUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMilAEKIENvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbhI2CgpjcmVkZW50aWFsGAIgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdHVzIpABChxSZWNvdmVyTWNwQ3JlZGVudGlhbFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbhI2CgpjcmVkZW50aWFsGAIgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdHVzIoYBChJNY3BDcmVkZW50aWFsU3RhdGUSOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMiOwoWTWNwQXBwcm92ZWREZXN0aW5hdGlvbhIPCgd6b25lX2lkGAEgASgJEhAKCGVuZHBvaW50GAIgASgJIrsBChVNY3BDbGllbnRSZWdpc3RyYXRpb24SCgoCaWQYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhQKDGNvbmZpZGVudGlhbBgDIAEoCBIUCgxyZWRpcmVjdF91cmkYBCABKAkSDgoGaXNzdWVyGAUgASgJEjcKCHJlc291cmNlGAYgASgLMiUuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZlZERlc3RpbmF0aW9uEg4KBnNjb3BlcxgHIAMoCSIrChJHZXRNY3BTZXR1cFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCSK8AgoTR2V0TWNwU2V0dXBSZXNwb25zZRIPCgdlbmFibGVkGAEgASgIEjsKDXJlZ2lzdHJhdGlvbnMYAiADKAsyJC5lbmdyYW0uYXBwLnYxLk1jcENsaWVudFJlZ2lzdHJhdGlvbhI7CgxkZXN0aW5hdGlvbnMYAyADKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SMgoHY3VycmVudBgEIAEoCzIhLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXRlEhoKEnRyYW5zaXRpb25fcGVuZGluZxgFIAEoCBIPCgdmbG93X2lkGAYgASgJEhcKD2Zsb3dfZXhwaXJlc19hdBgHIAEoCRIgChhjbGllbnRfc2VjcmV0X2NvbmZpZ3VyZWQYCCABKAgi1AEKH0NvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAMgASgNEg8KB2Zsb3dfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSDAoEY29kZRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIUCgxyZWRpcmVjdF91cmkYCCABKAlCCQoHX2lzc3VlciJsChtSZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAMgASgNIuoDChhNY3BBcHByb3ZhbFJldmlld0NvbnRleHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgFIAEoCRIVCg1hcmd1bWVudF9oYXNoGAYgASgJEhQKDGJpbmRpbmdfaGFzaBgHIAEoCRIVCg1jb25uZWN0aW9uX2lkGAggASgJEhIKCmNhdGFsb2dfaWQYCSABKAkSGAoQY2F0YWxvZ19yZXZpc2lvbhgKIAEoDRIXCg9jb25maWdfcmV2aXNpb24YCyABKA0SFwoPYXV0aF9nZW5lcmF0aW9uGAwgASgNEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYDSABKAkSFQoNdXBzdHJlYW1fbmFtZRgOIAEoCRIYChB0b29sX2ZpbmdlcnByaW50GA8gASgJEhYKDmNvbnRleHRfZGlnZXN0GBAgASgJEhgKEGpvdXJuYWxfcmV2aXNpb24YESABKA0SEgoKZXhwaXJlc19hdBgSIAEoCRIXCg9wb2xpY3lfcmV2aXNpb24YEyABKAkSFQoNYmluZGluZ19lcG9jaBgUIAEoCSK/AQoSTWNwQXBwcm92YWxSZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghkZWNpc2lvbhgCIAEoCRIQCghhY3Rvcl9pZBgDIAEoCRISCgpkZWNpZGVkX2F0GAQgASgJEhgKEGpvdXJuYWxfcmV2aXNpb24YBSABKA0SQQoQcmV2aWV3ZWRfY29udGV4dBgGIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IuEEChJNY3BFeGVjdXRpb25SZXZpZXcSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhUKDWNvbm5lY3Rpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgEIAEoDRIaChJyZXN1bHRfdW5hdmFpbGFibGUYBSABKAgSKgoGcmV2aWV3GAYgASgLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbBIXCg9hbGxvd2VkX2FjdGlvbnMYByADKAkSDgoGcmVhc29uGAggASgJEhQKDHRvb2xfY2FsbF9pZBgJIAEoCRIOCgZydW5faWQYCiABKAkSFgoOcmVxdWVzdF9kaWdlc3QYCyABKAkSFQoNdXBzdHJlYW1fbmFtZRgMIAEoCRIZChFhcHByb3ZhbF9kZWNpc2lvbhgNIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYDiABKAkSLQoNcmV2aWV3ZWRfdG9vbBgPIAEoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBIqCiJhcmd1bWVudF9jb25zdHJhaW50X2RvY3VtZW50c19qc29uGBAgAygJEhQKDGJpbmRpbmdfaGFzaBgRIAEoCRJCChhleHBlY3RlZF91bmtub3duX2NvbnRleHQYEiABKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFVua25vd25Db250ZXh0EkMKFGNhbmNlbGxhdGlvbl9yZWNlaXB0GBMgASgLMiUuZW5ncmFtLmFwcC52MS5NY3BDYW5jZWxsYXRpb25SZWNlaXB0IoQBChFNY3BVbmtub3duQ29udGV4dBIUCgxleGVjdXRpb25faWQYASABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgCIAEoDRIWCg5yZXF1ZXN0X2RpZ2VzdBgDIAEoCRIUCgxiaW5kaW5nX2hhc2gYBCABKAkSEQoJaW50ZW50X2lkGAUgASgJIq8BChZNY3BDYW5jZWxsYXRpb25SZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRINCgVjYXVzZRgDIAEoCRIhChlleHBlY3RlZF9qb3VybmFsX3JldmlzaW9uGAQgASgNEhgKEGpvdXJuYWxfcmV2aXNpb24YBSABKA0SDQoFc3RhdGUYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCSKfAQoUTWNwUmVzb2x1dGlvblJlY2VpcHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEg8KB291dGNvbWUYAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoCRI6ChBleHBlY3RlZF9jb250ZXh0GAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BVbmtub3duQ29udGV4dCKBAQoRTWNwQmluZGluZ1JlY2VpcHQSFAoMYmluZGluZ19oYXNoGAEgASgJEhUKDWJpbmRpbmdfZXBvY2gYAiABKAkSDwoHY3VycmVudBgDIAEoCBIVCg1zdGFsZV9yZWFzb25zGAQgAygJEhcKD2FsbG93ZWRfYWN0aW9ucxgFIAMoCSKAAgoZR2V0U2Vzc2lvbk1jcFN0YXRlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhoKEmFmdGVyX2V4ZWN1dGlvbl9pZBgCIAEoCRIRCglwYWdlX3NpemUYAyABKA0SHgoRcGxhbl90b29sX2NhbGxfaWQYBCABKAlIAIgBARIfChdhZnRlcl9iaW5kaW5nX3Rvb2xfbmFtZRgFIAEoCRIZChFiaW5kaW5nX3BhZ2Vfc2l6ZRgGIAEoDRIVCg1vbWl0X2JpbmRpbmdzGAcgASgIEhcKD29taXRfZXhlY3V0aW9ucxgIIAEoCEIUChJfcGxhbl90b29sX2NhbGxfaWQivwQKGkdldFNlc3Npb25NY3BTdGF0ZVJlc3BvbnNlEjkKD2JpbmRpbmdfcmVjZWlwdBgBIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwQmluZGluZ1JlY2VpcHQSNQoKZXhlY3V0aW9ucxgCIAMoCzIhLmVuZ3JhbS5hcHAudjEuTWNwRXhlY3V0aW9uUmV2aWV3Ei0KCWFwcHJvdmFscxgDIAMoCzIaLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWwSHAoUdW5rbm93bl93cml0ZV9ibG9ja3MYBCADKAkSQAoTcmVzb2x1dGlvbl9yZWNlaXB0cxgFIAMoCzIjLmVuZ3JhbS5hcHAudjEuTWNwUmVzb2x1dGlvblJlY2VpcHQSFwoPYWxsb3dlZF9hY3Rpb25zGAYgAygJEhQKDGF2YWlsYWJpbGl0eRgHIAEoCRIZChFuZXh0X2V4ZWN1dGlvbl9pZBgIIAEoCRI+Cgx3cml0ZV9wb2xpY3kYCSABKAsyKC5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVzcG9uc2USOQoNYmluZGluZ190b29scxgKIAMoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQm91bmRUb29sU3VtbWFyeRIeChZuZXh0X2JpbmRpbmdfdG9vbF9uYW1lGAsgASgJEh4KFmhhc19tb3JlX2JpbmRpbmdfdG9vbHMYDCABKAgSGwoTaGFzX21vcmVfZXhlY3V0aW9ucxgNIAEoCCLKAgoTTWNwQm91bmRUb29sU3VtbWFyeRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhgKEGNvbm5lY3Rpb25fYWxpYXMYAiABKAkSFwoPbW9kZWxfdG9vbF9uYW1lGAMgASgJEi0KDXJldmlld2VkX3Rvb2wYBCABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgFIAMoCRIYChBhdXRvbWF0aW9uX3dyaXRlGAYgASgIEhQKDHVucmVzdHJpY3RlZBgHIAEoCBISCgpjYXRhbG9nX2lkGAggASgJEhgKEGNhdGFsb2dfcmV2aXNpb24YCSABKA0SFwoPY29uZmlnX3JldmlzaW9uGAogASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgLIAEoDSrIAQoUTWNwUmViaW5kRHJhaW5SZWFzb24SJwojTUNQX1JFQklORF9EUkFJTl9SRUFTT05fVU5TUEVDSUZJRUQQABIwCixNQ1BfUkVCSU5EX0RSQUlOX1JFQVNPTl9ESVNQQVRDSF9JTl9QUk9HUkVTUxABEioKJk1DUF9SRUJJTkRfRFJBSU5fUkVBU09OX1VOS05PV05fSU5URU5UEAISKQolTUNQX1JFQklORF9EUkFJTl9SRUFTT05fTEVHQUNZX0lOVEVOVBADMtUXChJJbnRlZ3JhdGlvblNlcnZpY2USXQoOTGlzdENvbm5lY3RvcnMSJC5lbmdyYW0uYXBwLnYxLkxpc3RDb25uZWN0b3JzUmVxdWVzdBolLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXNwb25zZRJgCg9VcHNlcnRDb25uZWN0b3ISJS5lbmdyYW0uYXBwLnYxLlVwc2VydENvbm5lY3RvclJlcXVlc3QaJi5lbmdyYW0uYXBwLnYxLlVwc2VydENvbm5lY3RvclJlc3BvbnNlEmAKD0RlbGV0ZUNvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2UScgoVR2V0SW50ZWdyYXRpb25DYXRhbG9nEisuZW5ncmFtLmFwcC52MS5HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXF1ZXN0GiwuZW5ncmFtLmFwcC52MS5HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXNwb25zZRJmChFTZXRNaW50Q3JlZGVudGlhbBInLmVuZ3JhbS5hcHAudjEuU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEmwKE1VwbG9hZENvbm5lY3RvckxvZ28SKS5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXF1ZXN0GiouZW5ncmFtLmFwcC52MS5VcGxvYWRDb25uZWN0b3JMb2dvUmVzcG9uc2USWgoNVGVzdENvbm5lY3RvchIjLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXNwb25zZRJvChRTZXRDb25uZWN0b3JTZXR0aW5ncxIqLmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0GisuZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1Jlc3BvbnNlEmAKD0xpc3RDb25uZWN0aW9ucxIlLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3Rpb25zUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3Rpb25zUmVzcG9uc2USYwoQQ3JlYXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkNyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRJjChBVcGRhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuVXBkYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEERlbGV0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0aW9uUmVzcG9uc2USXQoOVGVzdENvbm5lY3Rpb24SJC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0aW9uUmVxdWVzdBolLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXNwb25zZRJvChRTZXRDb25uZWN0aW9uRW5hYmxlZBIqLmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdGlvbkVuYWJsZWRSZXF1ZXN0GisuZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEmwKE0dldEdvb2dsZUNsb3VkU2V0dXASKS5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXF1ZXN0GiouZW5ncmFtLmFwcC52MS5HZXRHb29nbGVDbG91ZFNldHVwUmVzcG9uc2USVAoLRGlzY292ZXJNY3ASIS5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyTWNwUmVxdWVzdBoiLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXNwb25zZRJRCgpQdWJsaXNoTWNwEiAuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVxdWVzdBohLmVuZ3JhbS5hcHAudjEuUHVibGlzaE1jcFJlc3BvbnNlEmMKEFB1dE1jcENyZWRlbnRpYWwSJi5lbmdyYW0uYXBwLnYxLlB1dE1jcENyZWRlbnRpYWxSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5QdXRNY3BDcmVkZW50aWFsUmVzcG9uc2USVAoLR2V0TWNwU2V0dXASIS5lbmdyYW0uYXBwLnYxLkdldE1jcFNldHVwUmVxdWVzdBoiLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXNwb25zZRJ7ChhDb21wbGV0ZU1jcEF1dGhvcml6YXRpb24SLi5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlcXVlc3QaLy5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlc3BvbnNlEm8KFFJlY292ZXJNY3BDcmVkZW50aWFsEiouZW5ncmFtLmFwcC52MS5SZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlJlY292ZXJNY3BDcmVkZW50aWFsUmVzcG9uc2USVwoMQXV0aG9yaXplTWNwEiIuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXNwb25zZRJaCg1EaXNjb25uZWN0TWNwEiMuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGlzY29ubmVjdE1jcFJlc3BvbnNlEmkKEkdldFNlc3Npb25NY3BTdGF0ZRIoLmVuZ3JhbS5hcHAudjEuR2V0U2Vzc2lvbk1jcFN0YXRlUmVxdWVzdBopLmVuZ3JhbS5hcHAudjEuR2V0U2Vzc2lvbk1jcFN0YXRlUmVzcG9uc2USYwoQTGlzdE1jcEFwcHJvdmFscxImLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRJmChFEZWNpZGVNY3BBcHByb3ZhbBInLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEloKDUNhbmNlbE1jcENhbGwSIy5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVzcG9uc2USZgoRUmVzb2x2ZU1jcE91dGNvbWUSJy5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRJjChBQcmVwYXJlTWNwUmViaW5kEiYuZW5ncmFtLmFwcC52MS5QcmVwYXJlTWNwUmViaW5kUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuUHJlcGFyZU1jcFJlYmluZFJlc3BvbnNlEk4KCVJlYmluZE1jcBIfLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVzcG9uc2ViBnByb3RvMw", [file_engram_app_v1_session]); /** * @generated from message engram.app.v1.GoogleCloudConnectionConfig @@ -1625,6 +1625,11 @@ export type CancelMcpCallRequest = Message<"engram.app.v1.CancelMcpCallRequest"> * @generated from field: string execution_id = 1; */ executionId: string; + + /** + * @generated from field: uint32 expected_journal_revision = 2; + */ + expectedJournalRevision: number; }; /** @@ -1642,6 +1647,11 @@ export type CancelMcpCallResponse = Message<"engram.app.v1.CancelMcpCallResponse * @generated from field: string state = 1; */ state: string; + + /** + * @generated from field: engram.app.v1.McpCancellationReceipt receipt = 2; + */ + receipt?: McpCancellationReceipt; }; /** @@ -1664,6 +1674,11 @@ export type ResolveMcpOutcomeRequest = Message<"engram.app.v1.ResolveMcpOutcomeR * @generated from field: string outcome = 2; */ outcome: string; + + /** + * @generated from field: engram.app.v1.McpUnknownContext expected_unknown_context = 3; + */ + expectedUnknownContext?: McpUnknownContext; }; /** @@ -1681,6 +1696,11 @@ export type ResolveMcpOutcomeResponse = Message<"engram.app.v1.ResolveMcpOutcome * @generated from field: string outcome = 1; */ outcome: string; + + /** + * @generated from field: engram.app.v1.McpResolutionReceipt receipt = 2; + */ + receipt?: McpResolutionReceipt; }; /** @@ -1690,6 +1710,154 @@ export type ResolveMcpOutcomeResponse = Message<"engram.app.v1.ResolveMcpOutcome export const ResolveMcpOutcomeResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_integration, 57); +/** + * @generated from message engram.app.v1.PrepareMcpRebindRequest + */ +export type PrepareMcpRebindRequest = Message<"engram.app.v1.PrepareMcpRebindRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string expected_binding_hash = 2; + */ + expectedBindingHash: string; +}; + +/** + * Describes the message engram.app.v1.PrepareMcpRebindRequest. + * Use `create(PrepareMcpRebindRequestSchema)` to create a new message. + */ +export const PrepareMcpRebindRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 58); + +/** + * @generated from message engram.app.v1.PrepareMcpRebindResponse + */ +export type PrepareMcpRebindResponse = Message<"engram.app.v1.PrepareMcpRebindResponse"> & { + /** + * @generated from field: string binding_hash = 1; + */ + bindingHash: string; + + /** + * @generated from field: string binding_epoch = 2; + */ + bindingEpoch: string; + + /** + * @generated from field: repeated engram.app.v1.McpBoundToolSummary binding_tools = 3; + */ + bindingTools: McpBoundToolSummary[]; + + /** + * @generated from field: repeated string deny_hosts = 4; + */ + denyHosts: string[]; + + /** + * @generated from field: string deny_revision = 5; + */ + denyRevision: string; + + /** + * @generated from field: string deny_digest = 6; + */ + denyDigest: string; + + /** + * @generated from field: repeated engram.app.v1.McpRebindConnection connections = 7; + */ + connections: McpRebindConnection[]; + + /** + * @generated from field: engram.app.v1.McpRebindDrain drain = 8; + */ + drain?: McpRebindDrain; +}; + +/** + * Describes the message engram.app.v1.PrepareMcpRebindResponse. + * Use `create(PrepareMcpRebindResponseSchema)` to create a new message. + */ +export const PrepareMcpRebindResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 59); + +/** + * @generated from message engram.app.v1.McpRebindDrain + */ +export type McpRebindDrain = Message<"engram.app.v1.McpRebindDrain"> & { + /** + * @generated from field: engram.app.v1.McpRebindDrainReason reason = 1; + */ + reason: McpRebindDrainReason; + + /** + * @generated from field: string retry_after = 2; + */ + retryAfter: string; +}; + +/** + * Describes the message engram.app.v1.McpRebindDrain. + * Use `create(McpRebindDrainSchema)` to create a new message. + */ +export const McpRebindDrainSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 60); + +/** + * @generated from message engram.app.v1.McpRebindConnection + */ +export type McpRebindConnection = Message<"engram.app.v1.McpRebindConnection"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: string alias = 2; + */ + alias: string; + + /** + * @generated from field: string endpoint = 3; + */ + endpoint: string; + + /** + * @generated from field: string zone = 4; + */ + zone: string; + + /** + * @generated from field: string credential_identity = 5; + */ + credentialIdentity: string; + + /** + * @generated from field: uint32 config_revision = 6; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 7; + */ + authGeneration: number; + + /** + * @generated from field: uint32 catalog_revision = 8; + */ + catalogRevision: number; +}; + +/** + * Describes the message engram.app.v1.McpRebindConnection. + * Use `create(McpRebindConnectionSchema)` to create a new message. + */ +export const McpRebindConnectionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 61); + /** * @generated from message engram.app.v1.RebindMcpRequest */ @@ -1703,6 +1871,11 @@ export type RebindMcpRequest = Message<"engram.app.v1.RebindMcpRequest"> & { * @generated from field: string expected_binding_hash = 2; */ expectedBindingHash: string; + + /** + * @generated from field: string expected_candidate_hash = 3; + */ + expectedCandidateHash: string; }; /** @@ -1710,7 +1883,7 @@ export type RebindMcpRequest = Message<"engram.app.v1.RebindMcpRequest"> & { * Use `create(RebindMcpRequestSchema)` to create a new message. */ export const RebindMcpRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 58); + messageDesc(file_engram_app_v1_integration, 62); /** * @generated from message engram.app.v1.RebindMcpResponse @@ -1727,7 +1900,7 @@ export type RebindMcpResponse = Message<"engram.app.v1.RebindMcpResponse"> & { * Use `create(RebindMcpResponseSchema)` to create a new message. */ export const RebindMcpResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 59); + messageDesc(file_engram_app_v1_integration, 63); /** * Entered values are request-only; responses contain metadata and references. @@ -1771,7 +1944,7 @@ export type PutMcpCredentialRequest = Message<"engram.app.v1.PutMcpCredentialReq * Use `create(PutMcpCredentialRequestSchema)` to create a new message. */ export const PutMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 60); + messageDesc(file_engram_app_v1_integration, 64); /** * @generated from message engram.app.v1.McpCredentialStatus @@ -1813,7 +1986,7 @@ export type McpCredentialStatus = Message<"engram.app.v1.McpCredentialStatus"> & * Use `create(McpCredentialStatusSchema)` to create a new message. */ export const McpCredentialStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 61); + messageDesc(file_engram_app_v1_integration, 65); /** * @generated from message engram.app.v1.PutMcpCredentialResponse @@ -1835,7 +2008,7 @@ export type PutMcpCredentialResponse = Message<"engram.app.v1.PutMcpCredentialRe * Use `create(PutMcpCredentialResponseSchema)` to create a new message. */ export const PutMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 62); + messageDesc(file_engram_app_v1_integration, 66); /** * @generated from message engram.app.v1.CompleteMcpAuthorizationResponse @@ -1857,7 +2030,7 @@ export type CompleteMcpAuthorizationResponse = Message<"engram.app.v1.CompleteMc * Use `create(CompleteMcpAuthorizationResponseSchema)` to create a new message. */ export const CompleteMcpAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 63); + messageDesc(file_engram_app_v1_integration, 67); /** * @generated from message engram.app.v1.RecoverMcpCredentialResponse @@ -1879,7 +2052,7 @@ export type RecoverMcpCredentialResponse = Message<"engram.app.v1.RecoverMcpCred * Use `create(RecoverMcpCredentialResponseSchema)` to create a new message. */ export const RecoverMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 64); + messageDesc(file_engram_app_v1_integration, 68); /** * @generated from message engram.app.v1.McpCredentialState @@ -1901,7 +2074,7 @@ export type McpCredentialState = Message<"engram.app.v1.McpCredentialState"> & { * Use `create(McpCredentialStateSchema)` to create a new message. */ export const McpCredentialStateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 65); + messageDesc(file_engram_app_v1_integration, 69); /** * @generated from message engram.app.v1.McpApprovedDestination @@ -1923,7 +2096,7 @@ export type McpApprovedDestination = Message<"engram.app.v1.McpApprovedDestinati * Use `create(McpApprovedDestinationSchema)` to create a new message. */ export const McpApprovedDestinationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 66); + messageDesc(file_engram_app_v1_integration, 70); /** * @generated from message engram.app.v1.McpClientRegistration @@ -1970,7 +2143,7 @@ export type McpClientRegistration = Message<"engram.app.v1.McpClientRegistration * Use `create(McpClientRegistrationSchema)` to create a new message. */ export const McpClientRegistrationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 67); + messageDesc(file_engram_app_v1_integration, 71); /** * @generated from message engram.app.v1.GetMcpSetupRequest @@ -1987,7 +2160,7 @@ export type GetMcpSetupRequest = Message<"engram.app.v1.GetMcpSetupRequest"> & { * Use `create(GetMcpSetupRequestSchema)` to create a new message. */ export const GetMcpSetupRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 68); + messageDesc(file_engram_app_v1_integration, 72); /** * @generated from message engram.app.v1.GetMcpSetupResponse @@ -2039,7 +2212,7 @@ export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & * Use `create(GetMcpSetupResponseSchema)` to create a new message. */ export const GetMcpSetupResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 69); + messageDesc(file_engram_app_v1_integration, 73); /** * @generated from message engram.app.v1.CompleteMcpAuthorizationRequest @@ -2091,7 +2264,7 @@ export type CompleteMcpAuthorizationRequest = Message<"engram.app.v1.CompleteMcp * Use `create(CompleteMcpAuthorizationRequestSchema)` to create a new message. */ export const CompleteMcpAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 70); + messageDesc(file_engram_app_v1_integration, 74); /** * @generated from message engram.app.v1.RecoverMcpCredentialRequest @@ -2118,7 +2291,7 @@ export type RecoverMcpCredentialRequest = Message<"engram.app.v1.RecoverMcpCrede * Use `create(RecoverMcpCredentialRequestSchema)` to create a new message. */ export const RecoverMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 71); + messageDesc(file_engram_app_v1_integration, 75); /** * Exact server review. Clients must return it unchanged when they decide. @@ -2232,7 +2405,7 @@ export type McpApprovalReviewContext = Message<"engram.app.v1.McpApprovalReviewC * Use `create(McpApprovalReviewContextSchema)` to create a new message. */ export const McpApprovalReviewContextSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 72); + messageDesc(file_engram_app_v1_integration, 76); /** * @generated from message engram.app.v1.McpApprovalReceipt @@ -2274,7 +2447,7 @@ export type McpApprovalReceipt = Message<"engram.app.v1.McpApprovalReceipt"> & { * Use `create(McpApprovalReceiptSchema)` to create a new message. */ export const McpApprovalReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 73); + messageDesc(file_engram_app_v1_integration, 77); /** * @generated from message engram.app.v1.McpExecutionReview @@ -2364,6 +2537,16 @@ export type McpExecutionReview = Message<"engram.app.v1.McpExecutionReview"> & { * @generated from field: string binding_hash = 17; */ bindingHash: string; + + /** + * @generated from field: engram.app.v1.McpUnknownContext expected_unknown_context = 18; + */ + expectedUnknownContext?: McpUnknownContext; + + /** + * @generated from field: engram.app.v1.McpCancellationReceipt cancellation_receipt = 19; + */ + cancellationReceipt?: McpCancellationReceipt; }; /** @@ -2371,7 +2554,91 @@ export type McpExecutionReview = Message<"engram.app.v1.McpExecutionReview"> & { * Use `create(McpExecutionReviewSchema)` to create a new message. */ export const McpExecutionReviewSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 74); + messageDesc(file_engram_app_v1_integration, 78); + +/** + * @generated from message engram.app.v1.McpUnknownContext + */ +export type McpUnknownContext = Message<"engram.app.v1.McpUnknownContext"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: uint32 journal_revision = 2; + */ + journalRevision: number; + + /** + * @generated from field: string request_digest = 3; + */ + requestDigest: string; + + /** + * @generated from field: string binding_hash = 4; + */ + bindingHash: string; + + /** + * @generated from field: string intent_id = 5; + */ + intentId: string; +}; + +/** + * Describes the message engram.app.v1.McpUnknownContext. + * Use `create(McpUnknownContextSchema)` to create a new message. + */ +export const McpUnknownContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 79); + +/** + * @generated from message engram.app.v1.McpCancellationReceipt + */ +export type McpCancellationReceipt = Message<"engram.app.v1.McpCancellationReceipt"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string actor_id = 2; + */ + actorId: string; + + /** + * @generated from field: string cause = 3; + */ + cause: string; + + /** + * @generated from field: uint32 expected_journal_revision = 4; + */ + expectedJournalRevision: number; + + /** + * @generated from field: uint32 journal_revision = 5; + */ + journalRevision: number; + + /** + * @generated from field: string state = 6; + */ + state: string; + + /** + * @generated from field: string created_at = 7; + */ + createdAt: string; +}; + +/** + * Describes the message engram.app.v1.McpCancellationReceipt. + * Use `create(McpCancellationReceiptSchema)` to create a new message. + */ +export const McpCancellationReceiptSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 80); /** * @generated from message engram.app.v1.McpResolutionReceipt @@ -2396,6 +2663,11 @@ export type McpResolutionReceipt = Message<"engram.app.v1.McpResolutionReceipt"> * @generated from field: string created_at = 4; */ createdAt: string; + + /** + * @generated from field: engram.app.v1.McpUnknownContext expected_context = 5; + */ + expectedContext?: McpUnknownContext; }; /** @@ -2403,7 +2675,7 @@ export type McpResolutionReceipt = Message<"engram.app.v1.McpResolutionReceipt"> * Use `create(McpResolutionReceiptSchema)` to create a new message. */ export const McpResolutionReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 75); + messageDesc(file_engram_app_v1_integration, 81); /** * @generated from message engram.app.v1.McpBindingReceipt @@ -2440,7 +2712,7 @@ export type McpBindingReceipt = Message<"engram.app.v1.McpBindingReceipt"> & { * Use `create(McpBindingReceiptSchema)` to create a new message. */ export const McpBindingReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 76); + messageDesc(file_engram_app_v1_integration, 82); /** * @generated from message engram.app.v1.GetSessionMcpStateRequest @@ -2492,7 +2764,7 @@ export type GetSessionMcpStateRequest = Message<"engram.app.v1.GetSessionMcpStat * Use `create(GetSessionMcpStateRequestSchema)` to create a new message. */ export const GetSessionMcpStateRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 77); + messageDesc(file_engram_app_v1_integration, 83); /** * @generated from message engram.app.v1.GetSessionMcpStateResponse @@ -2569,7 +2841,7 @@ export type GetSessionMcpStateResponse = Message<"engram.app.v1.GetSessionMcpSta * Use `create(GetSessionMcpStateResponseSchema)` to create a new message. */ export const GetSessionMcpStateResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 78); + messageDesc(file_engram_app_v1_integration, 84); /** * Immutable session grant. Schemas remain independent documents with their own roots. @@ -2638,7 +2910,38 @@ export type McpBoundToolSummary = Message<"engram.app.v1.McpBoundToolSummary"> & * Use `create(McpBoundToolSummarySchema)` to create a new message. */ export const McpBoundToolSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 79); + messageDesc(file_engram_app_v1_integration, 85); + +/** + * @generated from enum engram.app.v1.McpRebindDrainReason + */ +export enum McpRebindDrainReason { + /** + * @generated from enum value: MCP_REBIND_DRAIN_REASON_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: MCP_REBIND_DRAIN_REASON_DISPATCH_IN_PROGRESS = 1; + */ + DISPATCH_IN_PROGRESS = 1, + + /** + * @generated from enum value: MCP_REBIND_DRAIN_REASON_UNKNOWN_INTENT = 2; + */ + UNKNOWN_INTENT = 2, + + /** + * @generated from enum value: MCP_REBIND_DRAIN_REASON_LEGACY_INTENT = 3; + */ + LEGACY_INTENT = 3, +} + +/** + * Describes the enum engram.app.v1.McpRebindDrainReason. + */ +export const McpRebindDrainReasonSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_engram_app_v1_integration, 0); /** * ADR 0057 C3: orchestrator-native connector catalog CRUD (Plane B). Connectors @@ -2912,6 +3215,14 @@ export const IntegrationService: GenService<{ input: typeof ResolveMcpOutcomeRequestSchema; output: typeof ResolveMcpOutcomeResponseSchema; }, + /** + * @generated from rpc engram.app.v1.IntegrationService.PrepareMcpRebind + */ + prepareMcpRebind: { + methodKind: "unary"; + input: typeof PrepareMcpRebindRequestSchema; + output: typeof PrepareMcpRebindResponseSchema; + }, /** * @generated from rpc engram.app.v1.IntegrationService.RebindMcp */ diff --git a/orchestrator/src/mcp/active-watch.ts b/orchestrator/src/mcp/active-watch.ts new file mode 100644 index 000000000..d00bbd44a --- /dev/null +++ b/orchestrator/src/mcp/active-watch.ts @@ -0,0 +1,54 @@ +/** Replica-local abort signals. Authority remains in durable state, not this registry. */ +export function makeMcpActiveWatch(deps: { check(id: string): Promise; intervalMs: number }) { + const active = new Map>(); + return { + register(id: string) { + const controller = new AbortController(); + const controllers = active.get(id) ?? new Set(); + controllers.add(controller); + active.set(id, controllers); + let closed = false; + let failure: unknown; + let timer: ReturnType | undefined; + const check = async (): Promise => { + if (closed || controller.signal.aborted) return; + let timeout: ReturnType | undefined; + try { + await Promise.race([ + deps.check(id), + new Promise((_, reject) => { + timeout = setTimeout( + () => reject(Error("MCP authority check timed out")), + deps.intervalMs, + ); + }), + ]); + } catch (error) { + failure = error; + controller.abort(); + } finally { + if (timeout) clearTimeout(timeout); + } + if (!closed && !controller.signal.aborted) timer = setTimeout(check, deps.intervalMs); + }; + // Defer the first read until registration is visible to local cancellation. + const ready = Promise.resolve().then(check); + return { + signal: controller.signal, + ready, + throwIfFailed() { + if (failure !== undefined) throw failure; + }, + close() { + closed = true; + if (timer) clearTimeout(timer); + controllers.delete(controller); + if (!controllers.size && active.get(id) === controllers) active.delete(id); + }, + }; + }, + cancel(id: string) { + for (const controller of active.get(id) ?? []) controller.abort(); + }, + }; +} diff --git a/orchestrator/src/mcp/authority-error.ts b/orchestrator/src/mcp/authority-error.ts new file mode 100644 index 000000000..b809a64e0 --- /dev/null +++ b/orchestrator/src/mcp/authority-error.ts @@ -0,0 +1,2 @@ +/** An authoritative local mismatch, distinct from transport or storage failure. */ +export class McpAuthorityStale extends Error {} diff --git a/orchestrator/src/mcp/authority-notification.ts b/orchestrator/src/mcp/authority-notification.ts new file mode 100644 index 000000000..3b9b1efe5 --- /dev/null +++ b/orchestrator/src/mcp/authority-notification.ts @@ -0,0 +1,17 @@ +import { log } from "../log.ts"; + +// Notifications request a recheck; committed authority rows remain the source of truth. +const listeners = new Set<() => void>(); +export function observeMcpAuthorityChanges(listener: () => void): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} +export function notifyMcpAuthorityChanged(): void { + for (const listener of listeners) { + try { + listener(); + } catch { + log.warn("MCP authority notification unavailable"); + } + } +} diff --git a/orchestrator/src/mcp/binding-store.ts b/orchestrator/src/mcp/binding-store.ts index 77e8f5c6b..d4d7cc27c 100644 --- a/orchestrator/src/mcp/binding-store.ts +++ b/orchestrator/src/mcp/binding-store.ts @@ -1,3 +1,4 @@ +import { McpAuthorityStale } from "./authority-error.ts"; import { makeSessionBindingResolver } from "./session-resolver.ts"; import { makeSessionWritePolicy } from "./session-policy.ts"; import { and, eq } from "drizzle-orm"; @@ -58,7 +59,7 @@ export function restoreBindingSnapshot( binding.alias !== connection.alias || canonicalDigest(binding.config) !== canonicalDigest(connection.config) ) - throw new Error("MCP stored binding identity missing"); + throw new McpAuthorityStale("MCP stored binding identity missing"); immutableBindings.push(binding); } immutableBindings.sort((a, b) => @@ -69,7 +70,8 @@ export function restoreBindingSnapshot( immutableBindings: structuredClone(immutableBindings), }; const { bindingHash, ...body } = snapshot; - if (canonicalDigest(body) !== bindingHash) throw new Error("MCP stored binding hash mismatch"); + if (canonicalDigest(body) !== bindingHash) + throw new McpAuthorityStale("MCP stored binding hash mismatch"); return snapshot; } export async function readStoredBindingSession( @@ -100,10 +102,11 @@ export async function readStoredBindingSession( export async function readLiveBindingAuthority( profileId: string, operatorPolicies: readonly McpOperatorPolicy[], - db = getDb(), + db: ReturnType | import("./journal-review.ts").JournalTx = getDb(), ): Promise { const [currentProfile] = await db.select().from(profile).where(eq(profile.id, profileId)); - if (!currentProfile || currentProfile.deletedAt) throw new Error("MCP profile unavailable"); + if (!currentProfile || currentProfile.deletedAt) + throw new McpAuthorityStale("MCP profile unavailable"); const rows = await db .select() .from(integrationConnection) @@ -127,7 +130,7 @@ export async function readLiveBindingAuthority( catalog.credentialIdentity === row.credentialIdentity; const tools = available ? reviewCatalog(catalog.tools) : []; if (available && canonicalDigest(tools) !== canonicalDigest(catalog.tools)) - throw new Error("MCP catalog fingerprint mismatch"); + throw new McpAuthorityStale("MCP catalog fingerprint mismatch"); connections.push({ id: row.id, catalogId: catalog?.id ?? "", diff --git a/orchestrator/src/mcp/contracts.ts b/orchestrator/src/mcp/contracts.ts index 427694836..4a41eb5da 100644 --- a/orchestrator/src/mcp/contracts.ts +++ b/orchestrator/src/mcp/contracts.ts @@ -1,6 +1,11 @@ import { sessionMcpState, type McpReviewProducers } from "./review-state.ts"; import { toJson } from "@bufbuild/protobuf"; -import { McpApprovalReviewContextSchema } from "../gen/engram/app/v1/integration_pb.ts"; +import { + McpApprovalReviewContextSchema, + McpRebindDrainSchema, + McpRebindDrainReason, +} from "../gen/engram/app/v1/integration_pb.ts"; +import { McpRebindDraining, type McpRebindDrain } from "./rebind.ts"; import { policyReviewCurrent, type JournalReviewContext } from "./journal-review.ts"; import { credentialHandlers } from "./credential-rpc.ts"; import type { CredentialAdmin } from "./credential-admin.ts"; @@ -11,10 +16,9 @@ import type { IntegrationConnectionStore, } from "../db/integration-connections.ts"; import { requireAdmin, requireUser, type GetSession } from "../rpc/require.ts"; -import { assertOwnerOrAdmin } from "./approvals.ts"; import { validateMcpConfig, type McpConfig } from "./config.ts"; import type { McpRepository } from "./repository.ts"; -import type { McpJournal } from "./journal.ts"; +import type { McpJournal, McpUnknownContext } from "./journal.ts"; import type { McpDiscoveredTool, McpReviewedTool } from "./types.ts"; /** Internal services use trusted connection identities, never guest destinations. */ @@ -31,12 +35,19 @@ export interface McpServices { destinations?: { zoneId: string; endpoint: string }[]; runtime?: { cancel(executionId: string): Promise; - /** Must authorize the live owner/admin and acknowledge deny policy before activation. */ + /** Both operations authorize a fresh owner/admin; commit requires the reviewed candidate. */ + prepareRebind( + sessionId: string, + expectedBindingHash: string, + actorId: string, + ): Promise< + import("./session-bindings.ts").McpSessionBindingSnapshot & { drain?: McpRebindDrain } + >; rebind( sessionId: string, expectedBindingHash: string, + expectedCandidateHash: string, actorId: string, - isAdmin: boolean, ): Promise<{ bindingHash: string }>; }; } @@ -50,11 +61,24 @@ export function requireMcpHook(hook: T | undefined): T { throw new ConnectError("external MCP service is not configured", Code.FailedPrecondition); return hook; } +function drainProto(drain: McpRebindDrain) { + const reason = + drain.reason === "dispatch_in_progress" + ? McpRebindDrainReason.DISPATCH_IN_PROGRESS + : drain.reason === "unknown_intent" + ? McpRebindDrainReason.UNKNOWN_INTENT + : McpRebindDrainReason.LEGACY_INTENT; + return { reason, retryAfter: drain.retryAfter }; +} export async function mcpOperation(run: () => Promise): Promise { try { return await run(); } catch (error) { if (error instanceof ConnectError) throw error; + if (error instanceof McpRebindDraining) + throw new ConnectError("MCP calls are still draining", Code.FailedPrecondition, undefined, [ + { desc: McpRebindDrainSchema, value: drainProto(error.drain) }, + ]); const message = error instanceof Error ? error.message : "MCP operation failed"; const code = /owner or administrator/.test(message) ? Code.PermissionDenied @@ -153,6 +177,7 @@ type Methods = | "decideMcpApproval" | "cancelMcpCall" | "resolveMcpOutcome" + | "prepareMcpRebind" | "rebindMcp"; export function mcpHandlers(deps: { services?: McpServices; @@ -237,7 +262,7 @@ export function mcpHandlers(deps: { planToolCallId: req.planToolCallId, producers: service.reviewProducers, canCancel: Boolean(service.runtime?.cancel), - canRebind: Boolean(service.runtime?.rebind), + canRebind: Boolean(service.runtime?.prepareRebind && service.runtime?.rebind), afterBindingToolName: req.afterBindingToolName || undefined, bindingPageSize: req.bindingPageSize || undefined, omitBindings: req.omitBindings, @@ -322,9 +347,26 @@ export function mcpHandlers(deps: { const service = services(); const runtime = requireMcpHook(service.runtime); return mcpOperation(async () => { - const call = await service.journal.cancel(req.executionId, actor.id, deps.now()); + if (!req.expectedJournalRevision) + throw new ConnectError("expected journal revision is required", Code.InvalidArgument); + const call = await service.journal.cancel( + req.executionId, + actor.id, + deps.now(), + req.expectedJournalRevision, + ); + // The committed cancellation fences other replicas even if local I/O already ended. await runtime.cancel(call.id); - return { state: call.state }; + const receipt = await service.journal.cancellationReceipt(call.id); + if (!receipt) throw Error("cancellation receipt unavailable"); + return { + state: call.state, + receipt: { + ...receipt, + actorId: receipt.actorId ?? "", + createdAt: receipt.createdAt.toISOString(), + }, + }; }); }, async resolveMcpOutcome(req, ctx) { @@ -335,28 +377,108 @@ export function mcpHandlers(deps: { if (req.outcome !== "succeeded" && req.outcome !== "failed" && req.outcome !== "acknowledged") throw new ConnectError("invalid outcome resolution", Code.InvalidArgument); const outcome = req.outcome; - return mcpOperation(async () => ({ - outcome: ( - await service.journal.resolveUnknown(req.executionId, actor.id, outcome, deps.now()) - ).outcome, - })); + if (!req.expectedUnknownContext) + throw new ConnectError("exact unknown context is required", Code.InvalidArgument); + const context = req.expectedUnknownContext; + const expected: McpUnknownContext = { + executionId: context.executionId, + journalRevision: context.journalRevision, + requestDigest: context.requestDigest, + bindingHash: context.bindingHash, + intentId: context.intentId, + }; + return mcpOperation(async () => { + const receipt = await service.journal.resolveUnknown( + req.executionId, + actor.id, + outcome, + deps.now(), + expected, + ); + return { + outcome: receipt.outcome, + receipt: { + ...receipt, + createdAt: receipt.createdAt.toISOString(), + expectedContext: receipt.expectedContext ?? undefined, + }, + }; + }); }, - async rebindMcp(req, ctx) { + async prepareMcpRebind(req, ctx) { const actor = await requireUser(ctx, deps.getSession); - const service = services(); - const runtime = requireMcpHook(service.runtime); + if (actor.serviceAccount) + throw new ConnectError("a human decision is required", Code.PermissionDenied); + const runtime = requireMcpHook(services().runtime); if (!req.expectedBindingHash) throw new ConnectError("expected binding hash is required", Code.InvalidArgument); - const owner = requireMcpHook(service.sessionOwner); return mcpOperation(async () => { - assertOwnerOrAdmin(await owner(req.sessionId), actor.id, actor.role === "admin"); - return runtime.rebind( + const snapshot = await runtime.prepareRebind( req.sessionId, req.expectedBindingHash, actor.id, - actor.role === "admin", ); + const bindingTools = snapshot.immutableBindings + .flatMap((binding) => + binding.tools.map((bound) => ({ + connectionId: binding.connectionId, + connectionAlias: binding.alias, + modelToolName: bound.name, + reviewedTool: { + ...bound.tool, + inputSchemaJson: JSON.stringify(bound.tool.inputSchema), + outputSchemaJson: bound.tool.outputSchema + ? JSON.stringify(bound.tool.outputSchema) + : "", + }, + argumentConstraintDocumentsJson: bound.grant.argumentConstraintDocuments.map( + (document) => JSON.stringify(document), + ), + automationWrite: bound.grant.automationWrite, + unrestricted: bound.grant.unrestricted, + catalogId: binding.catalogId, + catalogRevision: binding.catalogRevision, + configRevision: binding.configRevision, + authGeneration: binding.authGeneration, + })), + ) + .sort((a, b) => + a.modelToolName < b.modelToolName ? -1 : a.modelToolName > b.modelToolName ? 1 : 0, + ); + return { + bindingHash: snapshot.bindingHash, + bindingEpoch: String(snapshot.bindingEpoch), + bindingTools, + drain: snapshot.drain ? drainProto(snapshot.drain) : undefined, + denyHosts: snapshot.managedDenyPolicy.hosts, + denyRevision: String(snapshot.managedDenyPolicy.revision), + denyDigest: snapshot.managedDenyPolicy.digest, + connections: snapshot.immutableBindings.map((binding) => ({ + connectionId: binding.connectionId, + alias: binding.alias, + endpoint: binding.config.endpoint, + zone: binding.config.zone, + credentialIdentity: binding.credentialIdentity, + configRevision: binding.configRevision, + authGeneration: binding.authGeneration, + catalogRevision: binding.catalogRevision, + })), + }; }); }, + async rebindMcp(req, ctx) { + const actor = await requireUser(ctx, deps.getSession); + if (actor.serviceAccount) + throw new ConnectError("a human decision is required", Code.PermissionDenied); + const runtime = requireMcpHook(services().runtime); + if (!req.expectedBindingHash || !req.expectedCandidateHash) + throw new ConnectError( + "binding and reviewed candidate hashes are required", + Code.InvalidArgument, + ); + return mcpOperation(() => + runtime.rebind(req.sessionId, req.expectedBindingHash, req.expectedCandidateHash, actor.id), + ); + }, }; } diff --git a/orchestrator/src/mcp/credential-store.ts b/orchestrator/src/mcp/credential-store.ts index fde66a6db..2f6d6a705 100644 --- a/orchestrator/src/mcp/credential-store.ts +++ b/orchestrator/src/mcp/credential-store.ts @@ -1,3 +1,4 @@ +import { notifyMcpAuthorityChanged } from "./authority-notification.ts"; import { and, eq, sql } from "drizzle-orm"; import { create, fromJson, toJson, type JsonObject } from "@bufbuild/protobuf"; import { getDb } from "../db/client.ts"; @@ -88,7 +89,7 @@ export function makeCredentialMetadataStore(db = getDb()) { callbackId?: string, ) { const target = validateMcpConfig(config); - return db.transaction(async (tx) => { + const result = await db.transaction(async (tx) => { const [row] = await tx .select() .from(connection) @@ -135,6 +136,8 @@ export function makeCredentialMetadataStore(db = getDb()) { config: target, }; }); + notifyMcpAuthorityChanged(); + return result; } async function stage(claim: CredentialClaim, metadata: CredentialMetadata, now: Date) { const safe = checkedMetadata({ ...metadata, transition: claim.metadata.transition }); @@ -212,6 +215,7 @@ export function makeCredentialMetadataStore(db = getDb()) { ) .returning(); if (!row) throw new Error("stale credential transition"); + notifyMcpAuthorityChanged(); return row; } return { @@ -232,10 +236,11 @@ export function makeCredentialMetadataStore(db = getDb()) { ) .returning({ id: connection.id }); if (!rows.length) throw new Error("stale credential transition"); + notifyMcpAuthorityChanged(); return true; }, async recover(id: string, revision: number, generation: number, actorId: string, now: Date) { - return db.transaction(async (tx) => { + const result = await db.transaction(async (tx) => { const [row] = await tx.select().from(connection).where(eq(connection.id, id)).for("update"); if ( !row || @@ -261,6 +266,8 @@ export function makeCredentialMetadataStore(db = getDb()) { config: validateMcpConfig(row.config), }; }); + notifyMcpAuthorityChanged(); + return result; }, }; } diff --git a/orchestrator/src/mcp/effect-driver.ts b/orchestrator/src/mcp/effect-driver.ts index e40aae3fa..f388dc921 100644 --- a/orchestrator/src/mcp/effect-driver.ts +++ b/orchestrator/src/mcp/effect-driver.ts @@ -1,3 +1,4 @@ +import { McpAuthorityStale } from "./authority-error.ts"; import { fromJson } from "@bufbuild/protobuf"; import { and, eq } from "drizzle-orm"; import { getDb } from "../db/client.ts"; @@ -15,6 +16,7 @@ import type { SessionPolicyControlClient, } from "./control-client.ts"; import { makeCredentialBroker } from "./credential-broker.ts"; +import { makeMcpActiveWatch } from "./active-watch.ts"; import { canonicalDigest, canonicalJson } from "./hash.ts"; import { makeMcpJournal, type McpLiveDispatchAuthorization } from "./journal.ts"; import { requestPolicyContext } from "./journal-review.ts"; @@ -160,6 +162,7 @@ export async function persistVerifiedOutcome( dispatched: Call, outcome: ReturnType, now: () => Date, + verified = true, ): Promise { // Cancellation can change dispatching to unknown once. Only the result CAS is retried. for (let attempt = 0; attempt < 2; attempt++) { @@ -167,7 +170,8 @@ export async function persistVerifiedOutcome( if (!latest || latest.intentId !== dispatched.intentId) throw Error("MCP result identity changed"); if (!["dispatching", "outcome_unknown"].includes(latest.state)) return latest; - if (latest.state === "outcome_unknown" && outcome.state === "outcome_unknown") return latest; + if (latest.state === "outcome_unknown" && (!verified || outcome.state === "outcome_unknown")) + return latest; try { return await journal.finish( dispatched.id, @@ -228,7 +232,8 @@ export function makeMcpEffectDriver(deps: { const status = await (resolved.reviewedTool.classification === "write" ? policy.validateDispatchFence(context, expected) : policy.validateExecutionIdentity(context, expected)); - if (status !== "current") throw new McpRequestDenied("authority_changed"); + if (status === "stale") throw new McpRequestDenied("authority_changed"); + if (status !== "current") throw Error("MCP policy fence unavailable"); } async function current(call: Call, signal?: AbortSignal) { const resolved = call.requestContext?.resolvedTool; @@ -237,11 +242,12 @@ export function makeMcpEffectDriver(deps: { enabled(resolved, signal); const snapshot = await deps.resolver.readCurrent(call.sessionId); publication(snapshot); - if (snapshot.bindingHash !== call.bindingHash) throw Error("MCP binding changed"); + if (snapshot.bindingHash !== call.bindingHash) + throw new McpAuthorityStale("MCP binding changed"); await db.transaction((tx) => lockRuntimeAuthority(tx, call, "share")); if ( call.classification === "write" && - (await journal.hasUnknown(call.sessionId, call.connectionId)) + (await journal.hasUnknown(call.sessionId, call.connectionId, deps.now())) ) throw Error("unknown write outcome blocks this connection"); await fence(requestPolicyContext(call.requestContext!), resolved, call.originalArguments); @@ -339,7 +345,41 @@ export function makeMcpEffectDriver(deps: { } return true; } + const activeWatch = makeMcpActiveWatch({ + intervalMs: 1_000, + async check(id) { + const call = await journal.get(id); + if (!call || !["waiting_approval", "authorized", "dispatching"].includes(call.state)) + throw Error("MCP execution no longer active"); + await current(call); + }, + }); return { + cancel(id: string) { + activeWatch.cancel(id); + }, + async reconcile(id: string, signal?: AbortSignal) { + signal?.throwIfAborted(); + const call = await journal.get(id); + if (!call || !["waiting_approval", "authorized", "dispatching"].includes(call.state)) return; + if (call.deadline <= deps.now()) { + signal?.throwIfAborted(); + activeWatch.cancel(id); + await journal.cancelInternal(id, "deadline", deps.now()); + return; + } + try { + await current(call, signal); + } catch (error) { + activeWatch.cancel(id); + signal?.throwIfAborted(); + if (error instanceof McpRequestDenied || error instanceof McpAuthorityStale) { + await journal.cancelInternal(id, "authority_changed", deps.now()); + return; + } + throw error; + } + }, async acceptToolRequest(input: McpToolRequest, trustedDeadline?: Date) { input = structuredClone(input); const existing = await lookup(input); @@ -377,7 +417,7 @@ export function makeMcpEffectDriver(deps: { if ( write && (context.acceptedWriteEligibility !== "write_eligible" || - (await journal.hasUnknown(input.sessionId, resolved.binding.connectionId))) + (await journal.hasUnknown(input.sessionId, resolved.binding.connectionId, deps.now()))) ) throw new McpRequestDenied("write_not_allowed"); const authorization = !write @@ -427,122 +467,138 @@ export function makeMcpEffectDriver(deps: { return call; }, async driveOnce(id: string, signal?: AbortSignal): Promise { - let call = await journal.get(id); - if (!call) throw Error("MCP call unavailable"); - if (!["waiting_approval", "authorized"].includes(call.state)) return call; - const resolved = await current(call, signal); + const watch = activeWatch.register(id); + signal = signal ? AbortSignal.any([signal, watch.signal]) : watch.signal; try { - if (!(await schema(call, resolved, signal))) return (await journal.get(id))!; - } catch (error) { - if (!(error instanceof InvalidArguments)) throw error; - return journal.rejectBeforeDispatch( - id, - { - requestDigest: call.requestDigest, - version: call.version, - reservation: { - reservationId: error.lease.reservationId, - expiresAt: error.lease.expiresAt, + let call = await journal.get(id); + if (!call) throw Error("MCP call unavailable"); + if (!["waiting_approval", "authorized"].includes(call.state)) return call; + await watch.ready; + watch.throwIfFailed(); + signal.throwIfAborted(); + const resolved = await current(call, signal); + try { + if (!(await schema(call, resolved, signal))) return (await journal.get(id))!; + } catch (error) { + if (!(error instanceof InvalidArguments)) throw error; + return journal.rejectBeforeDispatch( + id, + { + requestDigest: call.requestDigest, + version: call.version, + reservation: { + reservationId: error.lease.reservationId, + expiresAt: error.lease.expiresAt, + }, }, - }, - deps.now(), - ); - } - if (call.state === "waiting_approval") { - await journal.requestApproval(id, deps.deploymentId, deps.now()); - return (await journal.get(id))!; - } - await current(call, signal); - const [connection] = await db - .select() - .from(integrationConnection) - .where(eq(integrationConnection.id, call.connectionId)); - if (!connection?.mcpCredential.status || connection.mcpCredential.transition) - throw Error("MCP credential authority unavailable"); - const status = fromJson(CredentialStatusSchema, connection.mcpCredential.status); - if (status.context?.authorizationGeneration !== BigInt(call.authGeneration)) - throw Error("MCP credential authority changed"); - if ((await journal.admit(id, deps.deploymentId, deps.now())) === "queued") - return (await journal.get(id))!; - const lease = (await journal.getAdmission(id))!; - const worker = client(resolved, signal); - try { - const identity = await worker.identity(); + deps.now(), + ); + } + if (call.state === "waiting_approval") { + await journal.requestApproval(id, deps.deploymentId, deps.now()); + return (await journal.get(id))!; + } await current(call, signal); - const operation: Extract = { - kind: "execute", - endpoint: resolved.binding.config.endpoint, - tool: resolved.reviewedTool.upstreamName, - arguments: argumentsValue(call.originalArguments!), - inputSchema: resolved.reviewedTool.inputSchema, - ...(resolved.reviewedTool.outputSchema - ? { outputSchema: resolved.reviewedTool.outputSchema } - : {}), - argumentConstraintDocuments: resolved.effectiveGrant.argumentConstraintDocuments, - classification: resolved.reviewedTool.classification, - }; - const dispatchIntent = intent(call, lease, identity, operation); - const proof: McpLiveDispatchAuthorization = { - executionId: id, - requestDigest: call.requestDigest, - bindingHash: call.bindingHash, - principalId: call.principalId, - expectedJournalRevision: call.version, - reservationId: lease.reservationId, - reservationExpiresAt: lease.expiresAt, - }; - call = await journal.dispatch( - id, - { bootId: identity.bootId, intentId: dispatchIntent.id, expiresAt: lease.expiresAt }, - deps.now(), - proof, - ); - const dispatched = call; - const broker = makeCredentialBroker({ - control: deps.control, - now: () => deps.now().getTime(), - admission: { - async assertAdmitted() { - await current(dispatched, signal); - await journal.assertDispatchCurrent(id, proof, deps.now()); - }, - }, - worker, - async beforeDispatch() { - enabled(resolved, signal); - await fence( - requestPolicyContext(dispatched.requestContext!), - resolved, - dispatched.originalArguments!, - ); - // The broker checks delivery expiry next, then sends on the retained socket. - enabled(resolved, signal); - }, - }); - let outcome: ReturnType; + const [connection] = await db + .select() + .from(integrationConnection) + .where(eq(integrationConnection.id, call.connectionId)); + if (!connection?.mcpCredential.status || connection.mcpCredential.transition) + throw Error("MCP credential authority unavailable"); + const status = fromJson(CredentialStatusSchema, connection.mcpCredential.status); + if (status.context?.authorizationGeneration !== BigInt(call.authGeneration)) + throw Error("MCP credential authority changed"); + if ((await journal.admit(id, deps.deploymentId, deps.now())) === "queued") + return (await journal.get(id))!; + const lease = (await journal.getAdmission(id))!; + const worker = client(resolved, signal); try { - const response = await broker.dispatch({ - status, - identity, - intent: dispatchIntent, - operation, - }); - if (!response.ok) throw Error("MCP result unavailable"); - outcome = verifiedExecutionResult(response.result); - } catch { - outcome = { - state: call.classification === "write" ? "outcome_unknown" : "failed", - result: { - outcome: call.classification === "write" ? "outcome_unknown" : "failed", - delivery: "unavailable", - reason: "upstream_failure", - }, - resultUnavailable: false, + const identity = await worker.identity(); + await current(call, signal); + const operation: Extract = { + kind: "execute", + endpoint: resolved.binding.config.endpoint, + tool: resolved.reviewedTool.upstreamName, + arguments: argumentsValue(call.originalArguments!), + inputSchema: resolved.reviewedTool.inputSchema, + ...(resolved.reviewedTool.outputSchema + ? { outputSchema: resolved.reviewedTool.outputSchema } + : {}), + argumentConstraintDocuments: resolved.effectiveGrant.argumentConstraintDocuments, + classification: resolved.reviewedTool.classification, + }; + const dispatchIntent = intent(call, lease, identity, operation); + const proof: McpLiveDispatchAuthorization = { + executionId: id, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + principalId: call.principalId, + expectedJournalRevision: call.version, + reservationId: lease.reservationId, + reservationExpiresAt: lease.expiresAt, }; + call = await journal.dispatch( + id, + { bootId: identity.bootId, intentId: dispatchIntent.id, expiresAt: lease.expiresAt }, + deps.now(), + proof, + ); + const dispatched = call; + const broker = makeCredentialBroker({ + control: deps.control, + now: () => deps.now().getTime(), + admission: { + async assertAdmitted() { + await current(dispatched, signal); + await journal.assertDispatchCurrent(id, proof, deps.now()); + }, + }, + worker, + async beforeDispatch() { + enabled(resolved, signal); + await fence( + requestPolicyContext(dispatched.requestContext!), + resolved, + dispatched.originalArguments!, + ); + // The broker checks delivery expiry next, then sends on the retained socket. + enabled(resolved, signal); + }, + }); + let outcome: ReturnType; + let verified = true; + try { + const response = await broker.dispatch({ + status, + identity, + intent: dispatchIntent, + operation, + }); + if (!response.ok) throw Error("MCP result unavailable"); + outcome = verifiedExecutionResult(response.result); + } catch { + verified = false; + const latest = await journal.get(call.id); + if (!latest || latest.intentId !== dispatched.intentId) + throw Error("MCP result identity changed"); + const interrupted = signal?.aborted || latest.state === "outcome_unknown"; + const unknown = interrupted || call.classification === "write"; + outcome = { + state: unknown ? "outcome_unknown" : "failed", + result: { + outcome: unknown ? "outcome_unknown" : "failed", + delivery: "unavailable", + reason: interrupted ? "execution_interrupted" : "upstream_failure", + }, + resultUnavailable: false, + }; + } + return persistVerifiedOutcome(journal, dispatched, outcome, deps.now, verified); + } finally { + worker.close(); } - return persistVerifiedOutcome(journal, dispatched, outcome, deps.now); } finally { - worker.close(); + watch.close(); } }, }; diff --git a/orchestrator/src/mcp/journal-review.ts b/orchestrator/src/mcp/journal-review.ts index ab675cb01..9918b0862 100644 --- a/orchestrator/src/mcp/journal-review.ts +++ b/orchestrator/src/mcp/journal-review.ts @@ -1,3 +1,4 @@ +import { McpAuthorityStale } from "./authority-error.ts"; import { create, fromJson, @@ -128,21 +129,33 @@ export async function lockHumanSession( sessionId: string, actorId: string, includeAncestors = false, + sessionLock: "share" | "update" = "share", ) { const [actor] = await tx.select().from(user).where(eq(user.id, actorId)).for("share"); if (!actor || actor.banned || isServiceAccountEmail(actor.email)) - throw new Error("human session owner or administrator required"); - const { owner, session, ancestors } = await lockSessionChain(tx, sessionId, includeAncestors); + throw new McpAuthorityStale("human session owner or administrator required"); + const { owner, session, ancestors } = await lockSessionChain( + tx, + sessionId, + includeAncestors, + sessionLock, + ); if (actor.role !== "admin" && owner.createdByUserId !== actorId) - throw new Error("human session owner or administrator required"); + throw new McpAuthorityStale("human session owner or administrator required"); return { owner, session, actor, ancestors }; } -export async function lockSessionChain(tx: JournalTx, sessionId: string, includeAncestors = true) { +export async function lockSessionChain( + tx: JournalTx, + sessionId: string, + includeAncestors = true, + sessionLock: "share" | "update" = "share", +) { const chain: { sessionId: string; taskId: string; parentId: string | null }[] = []; let id: string | null = sessionId; const seen = new Set(); while (id) { - if (seen.has(id) || chain.length >= 32) throw new Error("stale MCP review ancestry"); + if (seen.has(id) || chain.length >= 32) + throw new McpAuthorityStale("stale MCP review ancestry"); seen.add(id); const refs = await tx .select({ taskId: taskSession.taskId, parentId: task.spawningSessionId }) @@ -150,7 +163,8 @@ export async function lockSessionChain(tx: JournalTx, sessionId: string, include .innerJoin(task, eq(task.id, taskSession.taskId)) .where(eq(taskSession.sessionId, id)) .limit(2); - if (refs.length !== 1) throw new Error("human session owner or administrator required"); + if (refs.length !== 1) + throw new McpAuthorityStale("human session owner or administrator required"); chain.push({ sessionId: id, ...refs[0]! }); id = includeAncestors ? refs[0]!.parentId : null; } @@ -158,7 +172,7 @@ export async function lockSessionChain(tx: JournalTx, sessionId: string, include const tasks = new Map(); for (const taskId of [...new Set(chain.map((ref) => ref.taskId))].sort()) { const [row] = await tx.select().from(task).where(eq(task.id, taskId)).for("share"); - if (!row) throw new Error("human session owner or administrator required"); + if (!row) throw new McpAuthorityStale("human session owner or administrator required"); tasks.set(taskId, row); } const sessions = new Map(); @@ -167,10 +181,10 @@ export async function lockSessionChain(tx: JournalTx, sessionId: string, include .select() .from(taskSession) .where(and(eq(taskSession.sessionId, ref.sessionId), eq(taskSession.taskId, ref.taskId))) - .for("share"); + .for(sessionLock); // Revalidate the discovered chain after all relevant writes are blocked. if (!row || tasks.get(ref.taskId)?.spawningSessionId !== ref.parentId) - throw new Error("stale MCP review ancestry"); + throw new McpAuthorityStale("stale MCP review ancestry"); sessions.set(ref.sessionId, row); } const owner = tasks.get(chain[0]!.taskId)!, @@ -191,7 +205,7 @@ export async function assertLocalReviewCurrent( connectionLock: "share" | "update" = "share", ) { if (!call.requestContext || call.originalArguments === null || !session.mcpBindingEnvelope) - throw new Error("original request context unavailable"); + throw new McpAuthorityStale("original request context unavailable"); const stored = restoreBindingSnapshot( session.mcpBindingEnvelope, session.integrationConnections ?? [], @@ -209,16 +223,17 @@ export async function assertLocalReviewCurrent( (binding) => canonicalDigest(binding) === canonicalDigest(bound.binding), ) ) - throw new Error("stale MCP review binding"); + throw new McpAuthorityStale("stale MCP review binding"); const snapshots = [stored]; const [leafTask] = await tx.select().from(task).where(eq(task.id, session.taskId)); let parentId = leafTask?.spawningSessionId; const seen = new Set([session.sessionId]); - if (stored.ancestorAuthorityRefs.length >= 32) throw new Error("stale MCP review ancestry"); + if (stored.ancestorAuthorityRefs.length >= 32) + throw new McpAuthorityStale("stale MCP review ancestry"); for (let i = 0; i < stored.ancestorAuthorityRefs.length; i++) { const expected = stored.ancestorAuthorityRefs[i]!; if (parentId !== expected.sessionId || seen.has(parentId)) - throw new Error("stale MCP review ancestry"); + throw new McpAuthorityStale("stale MCP review ancestry"); seen.add(parentId); const ancestor = ancestors[i]?.session, ancestorTask = ancestors[i]?.owner; @@ -228,7 +243,7 @@ export async function assertLocalReviewCurrent( ancestor.sessionId !== parentId || ["done", "failed", "cancelled"].includes(ancestorTask.status) ) - throw new Error("stale MCP review ancestry"); + throw new McpAuthorityStale("stale MCP review ancestry"); const snapshot = restoreBindingSnapshot( ancestor.mcpBindingEnvelope, ancestor.integrationConnections ?? [], @@ -239,12 +254,12 @@ export async function assertLocalReviewCurrent( canonicalDigest(snapshot.ancestorAuthorityRefs) !== canonicalDigest(stored.ancestorAuthorityRefs.slice(i + 1)) ) - throw new Error("stale MCP review ancestry"); + throw new McpAuthorityStale("stale MCP review ancestry"); snapshots.push(snapshot); parentId = ancestorTask.spawningSessionId; } if (parentId || ancestors.length !== stored.ancestorAuthorityRefs.length) - throw new Error("stale MCP review ancestry"); + throw new McpAuthorityStale("stale MCP review ancestry"); const profileIds = [...new Set(snapshots.map((snapshot) => snapshot.profileId))].sort(); for (const id of profileIds) { const [currentProfile] = await tx.select().from(profile).where(eq(profile.id, id)).for("share"); @@ -262,7 +277,7 @@ export async function assertLocalReviewCurrent( canonicalDigest(snapshot.profilePublication ?? null), ) ) - throw new Error("stale MCP review profile"); + throw new McpAuthorityStale("stale MCP review profile"); } const bindings = snapshots.flatMap((snapshot) => snapshot.immutableBindings); for (const id of [...new Set(bindings.map((binding) => binding.connectionId))].sort()) { @@ -300,7 +315,7 @@ export async function assertLocalReviewCurrent( !catalog.tools.some((tool) => canonicalDigest(tool) === canonicalDigest(bound.tool)), ) ) - throw new Error("stale MCP review connection"); + throw new McpAuthorityStale("stale MCP review connection"); } } } @@ -309,7 +324,7 @@ export function reviewContext( approval: typeof mcpApproval.$inferSelect, ): JournalReviewContext { if (!call.requestContext || call.originalArguments === null) - throw new Error("original request context unavailable"); + throw new McpAuthorityStale("original request context unavailable"); const { resolvedTool } = call.requestContext, policy = requestPolicyContext(call.requestContext); return { diff --git a/orchestrator/src/mcp/journal.ts b/orchestrator/src/mcp/journal.ts index c493885fb..47c9ab893 100644 --- a/orchestrator/src/mcp/journal.ts +++ b/orchestrator/src/mcp/journal.ts @@ -1,6 +1,6 @@ import { lockRuntimeAuthority } from "./runtime-authority.ts"; import { makeMcpAdmission, lockAdmissionScope as lockScope } from "./admission.ts"; -import { and, asc, eq, gt, inArray, isNull, lte, sql } from "drizzle-orm"; +import { and, asc, eq, gt, inArray, isNull, lte, or, sql } from "drizzle-orm"; import { getDb } from "../db/client.ts"; import { integrationConnection as connection, @@ -8,6 +8,7 @@ import { mcpExecution as execution, mcpApproval as approval, mcpResolution as resolution, + mcpCancellation as cancellation, mcpAdmission as admission, taskSession, } from "../db/schema.ts"; @@ -55,7 +56,20 @@ export type McpCallInput = Pick< requestPolicyContext: RequestPolicyContext; }; -async function unknown(tx: Tx | Db, sessionId: string, connectionId: string): Promise { +function unknownBlocks(now: Date) { + return or( + isNull(resolution.executionId), + isNull(execution.intentId), + isNull(execution.intentExpiresAt), + gt(execution.intentExpiresAt, now), + ); +} +async function unknown( + tx: Tx | Db, + sessionId: string, + connectionId: string, + now: Date, +): Promise { const rows = await tx .select({ id: execution.id }) .from(execution) @@ -66,7 +80,7 @@ async function unknown(tx: Tx | Db, sessionId: string, connectionId: string): Pr eq(execution.connectionId, connectionId), eq(execution.classification, "write"), eq(execution.state, "outcome_unknown"), - isNull(resolution.executionId), + unknownBlocks(now), ), ) .limit(1); @@ -84,8 +98,98 @@ export interface McpLiveDispatchAuthorization { reservationId: string; reservationExpiresAt: Date; } +export type McpUnknownContext = { + executionId: string; + journalRevision: number; + requestDigest: string; + bindingHash: string; + intentId: string; +}; +export function unknownContext(call: typeof execution.$inferSelect): McpUnknownContext { + if (call.state !== "outcome_unknown" || !call.intentId) + throw Error("call has no unknown outcome"); + return { + executionId: call.id, + journalRevision: call.version, + requestDigest: call.requestDigest, + bindingHash: call.bindingHash, + intentId: call.intentId, + }; +} +export type McpInternalCancellationCause = + | "interrupt" + | "end" + | "revoke" + | "kill" + | "deadline" + | "resume" + | "authority_changed"; export function makeMcpJournal(db = getDb()) { const phasedAdmission = makeMcpAdmission(db); + async function cancelExecution( + id: string, + authority: + | { kind: "human"; actorId: string; expectedJournalRevision: number } + | { kind: "internal"; cause: McpInternalCancellationCause }, + now: Date, + ) { + return db.transaction(async (tx) => { + const [identity] = await tx.select().from(execution).where(eq(execution.id, id)); + if (!identity) throw Error("call not found"); + if (authority.kind === "human") + await lockHumanSession(tx, identity.sessionId, authority.actorId); + await tx + .select() + .from(connection) + .where(eq(connection.id, identity.connectionId)) + .for("update"); + const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); + if (!call) throw Error("call not found"); + const [prior] = await tx.select().from(cancellation).where(eq(cancellation.executionId, id)); + if (authority.kind === "human") { + if ( + prior?.actorId === authority.actorId && + prior.expectedJournalRevision === authority.expectedJournalRevision + ) + return call; + if ( + !Number.isSafeInteger(authority.expectedJournalRevision) || + call.version !== authority.expectedJournalRevision + ) + throw Error("stale cancellation context"); + } + if (!["waiting_approval", "authorized", "dispatching"].includes(call.state)) { + if (authority.kind === "human") throw Error("stale cancellation context"); + return call; + } + const state = call.state === "dispatching" ? "outcome_unknown" : "canceled"; + const [updated] = await tx + .update(execution) + .set({ state, version: call.version + 1, updatedAt: now }) + .where(eq(execution.id, id)) + .returning(); + await tx.insert(cancellation).values({ + executionId: id, + actorId: authority.kind === "human" ? authority.actorId : null, + cause: authority.kind === "human" ? "human" : authority.cause, + expectedJournalRevision: call.version, + journalRevision: call.version + 1, + state, + createdAt: now, + }); + await tx.delete(admission).where(eq(admission.executionId, id)); + await tx + .update(approval) + .set({ + decision: "denied", + actorId: authority.kind === "human" ? authority.actorId : null, + decidedAt: now, + }) + .where(and(eq(approval.executionId, id), eq(approval.decision, "pending"))); + return updated!; + }); + } + return { admitSchema: (id: string, deploymentId: string, now: Date) => phasedAdmission.reserve(id, deploymentId, "schema", now), @@ -96,8 +200,8 @@ export function makeMcpJournal(db = getDb()) { async getAdmission(id: string) { return (await db.select().from(admission).where(eq(admission.executionId, id)))[0] ?? null; }, - hasUnknown(sessionId: string, connectionId: string) { - return unknown(db, sessionId, connectionId); + hasUnknown(sessionId: string, connectionId: string, now: Date = new Date()) { + return unknown(db, sessionId, connectionId, now); }, async create(input: McpCallInput, authorization: McpCallAuthorization, now: Date) { input = structuredClone(input); @@ -294,6 +398,7 @@ export function makeMcpJournal(db = getDb()) { sessionId: string, actorId: string, page: { after?: string; limit?: number } = {}, + now: Date = new Date(), ) { const limit = page.limit ?? 16; if ( @@ -306,10 +411,11 @@ export function makeMcpJournal(db = getDb()) { return db.transaction(async (tx) => { const { session } = await lockHumanSession(tx, sessionId, actorId); const rows = await tx - .select({ call: execution, approval, resolution }) + .select({ call: execution, approval, resolution, cancellation }) .from(execution) .leftJoin(approval, eq(approval.executionId, execution.id)) .leftJoin(resolution, eq(resolution.executionId, execution.id)) + .leftJoin(cancellation, eq(cancellation.executionId, execution.id)) .where( and( eq(execution.sessionId, sessionId), @@ -327,7 +433,7 @@ export function makeMcpJournal(db = getDb()) { eq(execution.sessionId, sessionId), eq(execution.classification, "write"), eq(execution.state, "outcome_unknown"), - isNull(resolution.executionId), + unknownBlocks(now), ), ) .limit(17); @@ -503,7 +609,7 @@ export function makeMcpJournal(db = getDb()) { throw new Error("tool is not approved for this operation"); if ( call.classification === "write" && - (await unknown(tx, call.sessionId, call.connectionId)) + (await unknown(tx, call.sessionId, call.connectionId, now)) ) throw new Error("unknown write outcome blocks this connection"); const [lease] = await tx @@ -642,7 +748,7 @@ export function makeMcpJournal(db = getDb()) { } if ( call.classification === "write" && - (await unknown(tx, call.sessionId, call.connectionId)) + (await unknown(tx, call.sessionId, call.connectionId, now)) ) throw new Error("unknown write outcome blocks this connection"); return call; @@ -767,42 +873,24 @@ export function makeMcpJournal(db = getDb()) { return row; }); }, - async cancel(id: string, actorId: string, now: Date) { - return db.transaction(async (tx) => { - const [identity] = await tx - .select({ connectionId: execution.connectionId, sessionId: execution.sessionId }) - .from(execution) - .where(eq(execution.id, id)); - if (!identity) throw new Error("call not found"); - await lockHumanSession(tx, identity.sessionId, actorId); - await tx - .select() - .from(connection) - .where(eq(connection.id, identity.connectionId)) - .for("update"); - const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); - - if (!["waiting_approval", "authorized", "dispatching"].includes(call!.state)) return call!; - const state: McpExecutionState = - call!.state === "dispatching" ? "outcome_unknown" : "canceled"; - const [updated] = await tx - .update(execution) - .set({ state, version: call!.version + 1, updatedAt: now }) - .where(eq(execution.id, id)) - .returning(); - await tx.delete(admission).where(eq(admission.executionId, id)); - await tx - .update(approval) - .set({ decision: "denied", actorId, decidedAt: now }) - .where(and(eq(approval.executionId, id), eq(approval.decision, "pending"))); - return updated!; - }); + cancel(id: string, actorId: string, now: Date, expectedJournalRevision: number) { + return cancelExecution(id, { kind: "human", actorId, expectedJournalRevision }, now); + }, + /** Trusted server lifecycle only; never supplied through a public request. */ + cancelInternal(id: string, cause: McpInternalCancellationCause, now: Date) { + return cancelExecution(id, { kind: "internal", cause }, now); + }, + async cancellationReceipt(id: string) { + return ( + (await db.select().from(cancellation).where(eq(cancellation.executionId, id)))[0] ?? null + ); }, async resolveUnknown( id: string, actorId: string, outcome: "succeeded" | "failed" | "acknowledged", now: Date, + expected: McpUnknownContext, ) { return db.transaction(async (tx) => { const [identity] = await tx.select().from(execution).where(eq(execution.id, id)); @@ -814,12 +902,25 @@ export function makeMcpJournal(db = getDb()) { .where(eq(connection.id, identity.connectionId)) .for("update"); const [call] = await tx.select().from(execution).where(eq(execution.id, id)).for("update"); - if (!call || call.state !== "outcome_unknown") - throw new Error("call has no unknown outcome"); + const [prior] = await tx.select().from(resolution).where(eq(resolution.executionId, id)); + if ( + prior && + prior.actorId === actorId && + prior.outcome === outcome && + canonicalDigest(prior.expectedContext) === canonicalDigest(expected) + ) + return prior; + if ( + !call || + call.state !== "outcome_unknown" || + !expected || + canonicalDigest(expected) !== canonicalDigest(unknownContext(call)) + ) + throw new Error("stale unknown outcome context"); const [row] = await tx .insert(resolution) - .values({ executionId: id, actorId, outcome, createdAt: now }) + .values({ executionId: id, actorId, outcome, expectedContext: expected, createdAt: now }) .onConflictDoNothing() .returning(); if (!row) throw new Error("outcome already resolved"); diff --git a/orchestrator/src/mcp/lifecycle-events.ts b/orchestrator/src/mcp/lifecycle-events.ts new file mode 100644 index 000000000..462adf060 --- /dev/null +++ b/orchestrator/src/mcp/lifecycle-events.ts @@ -0,0 +1,60 @@ +import { and, eq } from "drizzle-orm"; +import type { getDb } from "../db/client.ts"; +import { consumerCursor } from "../db/schema.ts"; +import { parseStatusChangeTo } from "../control-plane/session-events.ts"; + +export type LifecycleEvent = { idx: bigint; kind: string; payloadJson: string }; +const consumer = "mcp-lifecycle"; + +/** One ordered page. Current-authority reconciliation commits before the cursor moves to the next event. */ +export function makeMcpLifecycleEvents(deps: { + db: ReturnType; + now(): Date; + list(sessionId: string, after: bigint): Promise; + reconcile(sessionId: string, event: LifecycleEvent): Promise; +}) { + return { + async runOnce(sessionId: string) { + await deps.db + .insert(consumerCursor) + .values({ sessionId, consumer, lastIdx: -1n, updatedAt: deps.now() }) + .onConflictDoNothing(); + const [cursor] = await deps.db + .select() + .from(consumerCursor) + .where(and(eq(consumerCursor.sessionId, sessionId), eq(consumerCursor.consumer, consumer))); + let previous = cursor!.lastIdx; + const events = await deps.list(sessionId, previous); + if (events.length > 200) throw Error("MCP lifecycle page too large"); + let last = previous; + for (const event of events) { + if (event.idx <= 0n || event.idx <= last) throw Error("invalid MCP lifecycle event order"); + last = event.idx; + } + for (const event of events) { + const status = + event.kind === "status_changed" ? parseStatusChangeTo(event.payloadJson) : undefined; + if (event.kind === "status_changed" && !status) throw Error("invalid MCP lifecycle status"); + if ( + event.kind === "run_interrupted" || + event.kind === "resumed" || + (status && ["completed", "failed", "dead"].includes(status)) + ) + await deps.reconcile(sessionId, event); + const moved = await deps.db + .update(consumerCursor) + .set({ lastIdx: event.idx, updatedAt: deps.now() }) + .where( + and( + eq(consumerCursor.sessionId, sessionId), + eq(consumerCursor.consumer, consumer), + eq(consumerCursor.lastIdx, previous), + ), + ) + .returning({ lastIdx: consumerCursor.lastIdx }); + if (!moved.length) return; + previous = event.idx; + } + }, + }; +} diff --git a/orchestrator/src/mcp/lifecycle-interceptor.ts b/orchestrator/src/mcp/lifecycle-interceptor.ts new file mode 100644 index 000000000..2e0ddd0fd --- /dev/null +++ b/orchestrator/src/mcp/lifecycle-interceptor.ts @@ -0,0 +1,25 @@ +import type { Interceptor } from "@connectrpc/connect"; +import { SessionService } from "../gen/engram/app/v1/session_pb.ts"; +import type { McpInternalCancellationCause } from "./journal.ts"; + +const causes: Readonly> = { + Interrupt: "interrupt", + DeleteSession: "end", + Resume: "resume", + EvictLocal: "kill", + EvictIdle: "kill", +}; + +/** Trusted session transport: commit cancellation before the coordinator changes lifecycle. */ +export function makeMcpLifecycleInterceptor( + invalidate: (id: string, cause: McpInternalCancellationCause) => Promise, +): Interceptor { + return (next) => async (req) => { + if (!req.stream && req.service.typeName === SessionService.typeName) { + const cause = causes[req.method.name]; + const id = (req.message as { sessionId?: unknown } | null)?.sessionId; + if (cause && typeof id === "string" && id) await invalidate(id, cause); + } + return next(req); + }; +} diff --git a/orchestrator/src/mcp/lifecycle-invalidation.ts b/orchestrator/src/mcp/lifecycle-invalidation.ts new file mode 100644 index 000000000..76e5626b5 --- /dev/null +++ b/orchestrator/src/mcp/lifecycle-invalidation.ts @@ -0,0 +1,37 @@ +import { and, asc, eq, gt, inArray } from "drizzle-orm"; +import type { getDb } from "../db/client.ts"; +import { mcpExecution } from "../db/schema.ts"; +import { makeMcpJournal, type McpInternalCancellationCause } from "./journal.ts"; +import { MCP_LIMITS } from "./limits.ts"; + +/** Internal causes carry no human actor. Each cancellation is durable before local abort. */ +export function makeMcpLifecycleInvalidator(deps: { + db: ReturnType; + abort(id: string): Promise; + now?: () => Date; +}) { + const journal = makeMcpJournal(deps.db); + return async (sessionId: string, cause: McpInternalCancellationCause) => { + let after = ""; + for (;;) { + const rows = await deps.db + .select({ id: mcpExecution.id }) + .from(mcpExecution) + .where( + and( + eq(mcpExecution.sessionId, sessionId), + gt(mcpExecution.id, after), + inArray(mcpExecution.state, ["waiting_approval", "authorized", "dispatching"]), + ), + ) + .orderBy(asc(mcpExecution.id)) + .limit(MCP_LIMITS.queueSize); + for (const row of rows) { + await journal.cancelInternal(row.id, cause, (deps.now ?? (() => new Date()))()); + await deps.abort(row.id); + after = row.id; + } + if (rows.length < MCP_LIMITS.queueSize) return; + } + }; +} diff --git a/orchestrator/src/mcp/lifecycle-reconciler.ts b/orchestrator/src/mcp/lifecycle-reconciler.ts new file mode 100644 index 000000000..862dbefd8 --- /dev/null +++ b/orchestrator/src/mcp/lifecycle-reconciler.ts @@ -0,0 +1,137 @@ +import { and, asc, desc, eq, gt, lte, sql } from "drizzle-orm"; +import type { getDb } from "../db/client.ts"; +import { mcpExecution } from "../db/schema.ts"; +import type { makeMcpEffectDriver } from "./effect-driver.ts"; +import { MCP_LIMITS } from "./limits.ts"; + +const active = sql`${mcpExecution.state} IN ('waiting_approval','authorized','dispatching')`; + +/** Independently revisits all active executions; failures do not starve later rows. */ +export function makeMcpLifecycleReconciler(deps: { + db: ReturnType; + driver: Pick, "reconcile" | "cancel">; + timeoutMs?: number; + onSession?(sessionId: string): Promise; +}) { + let after = ""; + const pending = new Map; controller: AbortController }>(); + async function check(id: string) { + let operation = pending.get(id); + if (!operation) { + const controller = new AbortController(); + const promise = deps.driver.reconcile(id, controller.signal); + operation = { promise, controller }; + pending.set(id, operation); + void promise.then( + () => pending.delete(id), + () => pending.delete(id), + ); + } + let timer: ReturnType | undefined; + try { + await Promise.race([ + operation.promise, + new Promise((_, reject) => { + timer = setTimeout(() => { + operation!.controller.abort(); + deps.driver.cancel(id); + reject(Error("MCP authority check timed out")); + }, deps.timeoutMs ?? 5_000); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } + } + return { + async reconcileAuthorityChange() { + const [last] = await deps.db + .select({ id: mcpExecution.id }) + .from(mcpExecution) + .where(active) + .orderBy(desc(mcpExecution.id)) + .limit(1); + if (!last) return; + let cursor = ""; + let failed = false; + for (;;) { + const rows = await deps.db + .select({ id: mcpExecution.id }) + .from(mcpExecution) + .where(and(active, gt(mcpExecution.id, cursor), lte(mcpExecution.id, last.id))) + .orderBy(asc(mcpExecution.id)) + .limit(MCP_LIMITS.queueSize); + for (const row of rows) { + try { + await check(row.id); + } catch { + failed = true; + } + cursor = row.id; + } + if (rows.length < MCP_LIMITS.queueSize) break; + } + if (failed) throw Error("MCP authority reconciliation unavailable"); + }, + async runOnce() { + const rows = await deps.db + .select({ id: mcpExecution.id, sessionId: mcpExecution.sessionId }) + .from(mcpExecution) + .where(and(active, gt(mcpExecution.id, after))) + .orderBy(asc(mcpExecution.id)) + .limit(MCP_LIMITS.queueSize); + let failed = false; + const sessions = new Set(); + for (const row of rows) { + if (!sessions.has(row.sessionId)) { + sessions.add(row.sessionId); + try { + await deps.onSession?.(row.sessionId); + } catch { + failed = true; + } + } + try { + await check(row.id); + } catch { + failed = true; + } + after = row.id; + } + if (rows.length < MCP_LIMITS.queueSize) after = ""; + if (failed) throw Error("MCP lifecycle reconciliation unavailable"); + return rows.length === MCP_LIMITS.queueSize; + }, + /** A fixed upper key bounds one event's scan even as new calls arrive. */ + async reconcileSession(sessionId: string) { + const [last] = await deps.db + .select({ id: mcpExecution.id }) + .from(mcpExecution) + .where(and(eq(mcpExecution.sessionId, sessionId), active)) + .orderBy(desc(mcpExecution.id)) + .limit(1); + if (!last) return; + let cursor = ""; + for (;;) { + const rows = await deps.db + .select({ id: mcpExecution.id }) + .from(mcpExecution) + .where( + and( + eq(mcpExecution.sessionId, sessionId), + active, + gt(mcpExecution.id, cursor), + lte(mcpExecution.id, last.id), + ), + ) + .orderBy(asc(mcpExecution.id)) + .limit(MCP_LIMITS.queueSize); + for (const row of rows) { + await check(row.id); + cursor = row.id; + } + if (rows.length < MCP_LIMITS.queueSize) return; + } + }, + }; +} diff --git a/orchestrator/src/mcp/rebind-deny.ts b/orchestrator/src/mcp/rebind-deny.ts new file mode 100644 index 000000000..2a222f5ea --- /dev/null +++ b/orchestrator/src/mcp/rebind-deny.ts @@ -0,0 +1,96 @@ +import { isIP } from "node:net"; +import { canonicalDigest } from "./hash.ts"; +import type { ManagedDenyPolicy } from "./session-bindings.ts"; +import { assertManagedDenyAck, type ManagedDenyAck } from "./session-resolver.ts"; + +/** Trusted host read must return the complete applied policy and its physical binding identity. */ +export interface AppliedMcpDeny { + sessionId: string; + bindingEpoch: number; + bindingHash: string; + policy: ManagedDenyPolicy; +} +function valid(policy: ManagedDenyPolicy): boolean { + const { digest, ...body } = policy; + return ( + policy.version === 1 && + Number.isSafeInteger(policy.revision) && + policy.revision >= 0 && + Array.isArray(policy.hosts) && + policy.hosts.every( + (host) => + typeof host === "string" && + host.length <= 253 && + !isIP(host) && + host.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)), + ) && + policy.hosts.every((host, index) => index === 0 || policy.hosts[index - 1]! < host) && + canonicalDigest(body) === digest + ); +} +/** Never drop a host-applied policy after a lost acknowledgement or failed commit. */ +export function prepareRebindDeny(input: { + sessionId: string; + bindingEpoch: number; + old: ManagedDenyPolicy; + next: ManagedDenyPolicy; + host: AppliedMcpDeny; +}): ManagedDenyPolicy { + const { host } = input; + if ( + !input.sessionId || + !Number.isSafeInteger(input.bindingEpoch) || + input.bindingEpoch < 0 || + host.sessionId !== input.sessionId || + host.bindingEpoch !== input.bindingEpoch || + !/^sha256:[a-f0-9]{64}$/.test(host.bindingHash) || + !valid(input.old) || + !valid(input.next) || + !valid(host.policy) + ) + throw Error("MCP applied deny state unavailable"); + const revision = Math.max(input.old.revision, input.next.revision, host.policy.revision) + 1; + if (!Number.isSafeInteger(revision)) throw Error("MCP deny revision exhausted"); + const body = { + version: 1 as const, + revision, + hosts: [...new Set([...input.old.hosts, ...input.next.hosts, ...host.policy.hosts])].sort(), + }; + return { ...body, digest: canonicalDigest(body) }; +} + +/** Task 6 supplies authenticated host operations. readApplied returns the full durable policy. + * compareAndApply atomically checks the complete previous identity and policy, installs and + * persists the new deny set, closes affected connections, then returns the exact ACK. It must + * not callback into O or hold a coordinator transaction that waits for O. A timeout can + * leave the policy installed; the next read must return that complete installed superset. */ +export interface McpRebindHost { + readApplied(sessionId: string, bindingEpoch: number): Promise; + compareAndApply( + previous: AppliedMcpDeny, + expected: ManagedDenyAck, + policy: ManagedDenyPolicy, + ): Promise; +} +export async function applyRebindDeny( + host: Pick | undefined, + previous: AppliedMcpDeny, + expected: ManagedDenyAck, + policy: ManagedDenyPolicy, +): Promise { + if ( + !host || + !valid(previous.policy) || + !valid(policy) || + expected.sessionId !== previous.sessionId || + expected.bindingEpoch !== previous.bindingEpoch || + expected.denyRevision !== policy.revision || + expected.denyDigest !== policy.digest || + policy.revision <= previous.policy.revision || + !previous.policy.hosts.every((value) => policy.hosts.includes(value)) || + !/^sha256:[a-f0-9]{64}$/.test(expected.bindingHash) + ) + throw Error("MCP deny application unavailable"); + const ack = await host.compareAndApply(previous, expected, policy); + assertManagedDenyAck(expected, ack); +} diff --git a/orchestrator/src/mcp/rebind.ts b/orchestrator/src/mcp/rebind.ts new file mode 100644 index 000000000..6930473e6 --- /dev/null +++ b/orchestrator/src/mcp/rebind.ts @@ -0,0 +1,252 @@ +import { and, asc, eq, gt, inArray, isNull, or } from "drizzle-orm"; +import { getDb } from "../db/client.ts"; +import { + integrationConnection, + mcpExecution, + profile, + taskSession, + type IntegrationConnectionSnapshot, +} from "../db/schema.ts"; +import { lockAdmissionScope } from "./admission.ts"; +import { + bindingSnapshotColumns, + readLiveBindingAuthority, + restoreBindingSnapshot, +} from "./binding-store.ts"; +import { canonicalDigest } from "./hash.ts"; +import { lockHumanSession } from "./journal-review.ts"; +import { applyRebindDeny, prepareRebindDeny, type McpRebindHost } from "./rebind-deny.ts"; +import { + compileMcpManifest, + compileSessionBindings, + managedAuthorityHash, + type McpOperatorPolicy, + type McpSessionBindingSnapshot, +} from "./session-bindings.ts"; +import { expectedManagedDenyAck } from "./session-resolver.ts"; + +export type McpRebindDrain = { + reason: "dispatch_in_progress" | "unknown_intent" | "legacy_intent"; + retryAfter: string; +}; +export class McpRebindDraining extends Error { + constructor(readonly drain: McpRebindDrain) { + super("MCP dispatch in progress"); + } +} + +/** External operations under the session lock have a hard limit. Host operations must not callback into O. */ +async function bounded(operation: () => Promise): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + operation(), + new Promise((_, reject) => { + timer = setTimeout(() => reject(Error("MCP rebind service unavailable")), 5_000); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} + +export function makeMcpRebind(deps: { + db?: ReturnType; + deploymentId: string; + host?: McpRebindHost; + readOperatorPolicies(): Promise; + readBindingEpoch(id: string): Promise; + readParentCurrent(id: string): Promise; + now?: () => Date; +}) { + const db = deps.db ?? getDb(); + async function run(sessionId: string, oldHash: string, actorId: string, candidateHash?: string) { + if (!sessionId || !oldHash || !actorId || !deps.host) + throw Error("MCP rebind service unavailable"); + const host = deps.host; + return db.transaction(async (tx) => { + await lockAdmissionScope(tx, deps.deploymentId); + const { session, owner, ancestors } = await lockHumanSession( + tx, + sessionId, + actorId, + true, + "update", + ); + if ( + ["done", "failed", "cancelled"].includes(owner.status) || + !session.mcpBindingEnvelope || + !session.profileId + ) + throw Error("MCP session unavailable"); + const old = restoreBindingSnapshot( + session.mcpBindingEnvelope, + session.integrationConnections ?? [], + ); + if (old.bindingHash !== oldHash) throw Error("stale MCP binding"); + if ( + old.profileId !== session.profileId || + old.ancestorAuthorityRefs.length !== ancestors.length || + old.ancestorAuthorityRefs.some( + (ref, index) => ref.sessionId !== ancestors[index]?.session.sessionId, + ) + ) + throw Error("MCP immutable ancestry changed"); + const profileIds = [ + ...new Set([ + session.profileId, + ...ancestors.map((row) => row.session.profileId).filter((id): id is string => !!id), + ]), + ].sort(); + await tx + .select() + .from(profile) + .where(inArray(profile.id, profileIds)) + .orderBy(asc(profile.id)) + .for("share"); + const connections = await tx + .select() + .from(integrationConnection) + .where(eq(integrationConnection.provider, "mcp")) + .orderBy(asc(integrationConnection.id)) + .for("share"); + // A broker may already be beyond its last database check. The original intent + // expiry bounds credential delivery even after cancellation or manual resolution. + const [pending] = await tx + .select({ + state: mcpExecution.state, + intentId: mcpExecution.intentId, + intentExpiresAt: mcpExecution.intentExpiresAt, + }) + .from(mcpExecution) + .where( + and( + eq(mcpExecution.sessionId, sessionId), + or( + eq(mcpExecution.state, "dispatching"), + and( + eq(mcpExecution.state, "outcome_unknown"), + or( + isNull(mcpExecution.intentId), + isNull(mcpExecution.intentExpiresAt), + gt(mcpExecution.intentExpiresAt, (deps.now ?? (() => new Date()))()), + ), + ), + ), + ), + ) + .limit(1) + .for("share"); + const drain: McpRebindDrain | undefined = pending + ? { + reason: + !pending.intentId || !pending.intentExpiresAt + ? "legacy_intent" + : pending.state === "dispatching" + ? "dispatch_in_progress" + : "unknown_intent", + retryAfter: + pending.intentId && pending.intentExpiresAt + ? pending.intentExpiresAt.toISOString() + : "", + } + : undefined; + if (candidateHash !== undefined && drain) throw new McpRebindDraining(drain); + const policies = await bounded(deps.readOperatorPolicies); + const live = await readLiveBindingAuthority(session.profileId, policies, tx); + if ( + !live.profilePublication || + live.profilePublication.grantsHash !== + canonicalDigest(live.profileGrants.filter((grant) => grant.mcp !== undefined)) + ) + throw Error("MCP grant publication unavailable"); + const epoch = await bounded(() => deps.readBindingEpoch(sessionId)); + const applied = await bounded(() => host.readApplied(sessionId, epoch)); + let parentAuthority: { sessionId: string; snapshot: McpSessionBindingSnapshot } | undefined; + if (owner.spawningSessionId) { + const parent = await bounded(() => deps.readParentCurrent(owner.spawningSessionId!)); + const stored = ancestors[0]?.session; + if ( + !stored?.mcpBindingEnvelope || + stored.sessionId !== owner.spawningSessionId || + restoreBindingSnapshot(stored.mcpBindingEnvelope, stored.integrationConnections ?? []) + .bindingHash !== parent.bindingHash + ) + throw Error("MCP parent authority changed"); + parentAuthority = { sessionId: owner.spawningSessionId, snapshot: parent }; + } else if (ancestors.length) throw Error("MCP parent authority changed"); + const requested = parentAuthority + ? live.profileGrants.filter( + (grant) => + grant.mcp && + parentAuthority!.snapshot.immutableBindings.some( + (binding) => + binding.connectionId === grant.connectionId && + binding.tools.some((tool) => tool.grant.upstreamName === grant.mcp!.upstreamName), + ), + ) + : live.profileGrants; + const compiled = compileSessionBindings({ + launchPolicy: { profileId: session.profileId, integrationGrants: live.profileGrants }, + liveAuthority: live, + bindingEpoch: epoch, + denyRevision: old.managedDenyPolicy.revision, + requestedGrants: requested, + parentAuthority, + }); + const deny = prepareRebindDeny({ + sessionId, + bindingEpoch: epoch, + old: old.managedDenyPolicy, + next: compiled.managedDenyPolicy, + host: applied, + }); + const { bindingHash: _hash, ...body } = { ...compiled, managedDenyPolicy: deny }; + const candidate = { ...body, bindingHash: canonicalDigest(body) }; + compileMcpManifest(candidate); + if (candidateHash !== undefined) { + if (candidate.bindingHash !== candidateHash) throw Error("stale MCP rebind candidate"); + await bounded(() => + applyRebindDeny(host, applied, expectedManagedDenyAck(sessionId, candidate), deny), + ); + if ((await bounded(() => deps.readBindingEpoch(sessionId))) !== epoch) + throw Error("MCP placement changed"); + const currentPolicies = await bounded(deps.readOperatorPolicies); + const currentAuthority = await readLiveBindingAuthority( + session.profileId, + currentPolicies, + tx, + ); + if (managedAuthorityHash(currentAuthority) !== candidate.managedAuthorityHash) + throw Error("MCP managed authority changed"); + const entries: IntegrationConnectionSnapshot[] = [ + ...(session.integrationConnections ?? []).filter((row) => row.provider !== "mcp"), + ...connections.map((row) => ({ + id: row.id, + alias: row.alias, + provider: row.provider, + displayName: row.displayName, + config: row.config, + })), + ]; + const columns = bindingSnapshotColumns(candidate, entries); + const updated = await tx + .update(taskSession) + .set(columns) + .where(and(eq(taskSession.taskId, session.taskId), eq(taskSession.sessionId, sessionId))) + .returning({ id: taskSession.sessionId }); + if (updated.length !== 1) throw Error("MCP session changed"); + } + return { ...candidate, drain }; + }); + } + return { + prepare(sessionId: string, oldHash: string, actorId: string) { + return run(sessionId, oldHash, actorId); + }, + commit(sessionId: string, oldHash: string, candidateHash: string, actorId: string) { + if (!candidateHash) throw Error("MCP reviewed candidate required"); + return run(sessionId, oldHash, actorId, candidateHash); + }, + }; +} diff --git a/orchestrator/src/mcp/recovery-scanner.ts b/orchestrator/src/mcp/recovery-scanner.ts index 63d1e5ee6..c7e411aa9 100644 --- a/orchestrator/src/mcp/recovery-scanner.ts +++ b/orchestrator/src/mcp/recovery-scanner.ts @@ -15,6 +15,8 @@ export function makeMcpRecoveryScanner(deps: { let timer: ReturnType | undefined; let running: Promise | undefined; let stopped = true; + let lifecycleTimer: ReturnType | undefined; + let lifecycleRunning: Promise | undefined; async function runOnce() { const journal = deps.journal ?? makeMcpJournal(); const now = (deps.now ?? (() => new Date()))(); @@ -41,18 +43,36 @@ export function makeMcpRecoveryScanner(deps: { running = tick(); }, 1_000); } + async function runLifecycleOnce() { + await (deps.runtime ?? mcpRecovery)()?.reconcileLifecycle(); + } + async function lifecycleTick() { + try { + await runLifecycleOnce(); + } catch { + log.warn("MCP lifecycle reconciliation unavailable"); + } + if (!stopped) + lifecycleTimer = setTimeout(() => { + lifecycleRunning = lifecycleTick(); + }, 1_000); + } return { runOnce, + runLifecycleOnce, async start() { if (!stopped) return; stopped = false; running = tick(); + lifecycleRunning = lifecycleTick(); await running; }, async stop() { stopped = true; clearTimeout(timer); + clearTimeout(lifecycleTimer); await running; + await lifecycleRunning; }, }; } diff --git a/orchestrator/src/mcp/recovery.ts b/orchestrator/src/mcp/recovery.ts index 650a04bcd..42579ddb4 100644 --- a/orchestrator/src/mcp/recovery.ts +++ b/orchestrator/src/mcp/recovery.ts @@ -14,6 +14,9 @@ import { import type { CuratedEvent } from "../control-plane/session-events.ts"; import { makeMcpJournal } from "./journal.ts"; import { MCP_LIMITS } from "./limits.ts"; +import { makeMcpLifecycleEvents, type LifecycleEvent } from "./lifecycle-events.ts"; +import { observeMcpAuthorityChanges } from "./authority-notification.ts"; +import { makeMcpLifecycleReconciler } from "./lifecycle-reconciler.ts"; /** Only references cross the durable workflow boundary. */ export interface McpExecutionMarker { @@ -64,9 +67,31 @@ export function makeMcpRecovery(deps: { completer?: ToolCallCompleter; now(): Date; readRequestEvent?: (sessionId: string, index: bigint) => Promise; + listLifecycleEvents?: (sessionId: string, after: bigint) => Promise; }) { const db = deps.db ?? getDb(); const journal = makeMcpJournal(db); + const reconciler = makeMcpLifecycleReconciler({ + db, + driver: deps.driver, + onSession: (id) => lifecycle.runOnce(id), + }); + const lifecycle = makeMcpLifecycleEvents({ + db, + now: deps.now, + async list(sessionId, after) { + if (deps.listLifecycleEvents) return deps.listLifecycleEvents(sessionId, after); + const page = await sessions.listSessionEvents( + { sessionId, afterIdx: after, limit: 200n }, + { signal: AbortSignal.timeout(5_000) }, + ); + return page.events.map((event) => { + if (event.idx === undefined) throw Error("MCP lifecycle event index unavailable"); + return { idx: event.idx, kind: event.kind, payloadJson: event.payloadJson }; + }); + }, + reconcile: (sessionId) => reconciler.reconcileSession(sessionId), + }); async function existing(ref: Pick) { return ( await db @@ -81,6 +106,13 @@ export function makeMcpRecovery(deps: { )[0]; } return { + reconcileLifecycle: async () => { + await reconciler.runOnce(); + }, + async reconcileAuthorityChange() { + await reconciler.reconcileAuthorityChange(); + }, + abortExecution: (id: string) => deps.driver.cancel(id), /** The durable step records only this decision. It never runs a provider operation. */ async decideRequest(ref: McpRequestMarker): Promise { try { @@ -269,6 +301,25 @@ let runtime: McpRecovery | undefined; export function installMcpRecovery(value: McpRecovery): void { if (runtime) throw Error("MCP runtime already installed"); runtime = value; + let pending = false; + let running = false; + observeMcpAuthorityChanges(() => { + pending = true; + if (running) return; + running = true; + void (async () => { + try { + while (pending) { + pending = false; + await value.reconcileAuthorityChange(); + } + } catch { + log.warn("MCP authority reconciliation unavailable"); + } finally { + running = false; + } + })(); + }); } export function mcpRecovery(): McpRecovery | undefined { return runtime; diff --git a/orchestrator/src/mcp/repository.ts b/orchestrator/src/mcp/repository.ts index 8b6770d84..a430bdc5f 100644 --- a/orchestrator/src/mcp/repository.ts +++ b/orchestrator/src/mcp/repository.ts @@ -1,3 +1,4 @@ +import { notifyMcpAuthorityChanged } from "./authority-notification.ts"; import { and, desc, eq, sql } from "drizzle-orm"; import { getDb } from "../db/client.ts"; import { integrationConnection as connection, mcpCatalog as catalog } from "../db/schema.ts"; @@ -37,6 +38,7 @@ export function makeMcpRepository(db = getDb()) { ) .returning(); if (!rows[0]) throw new Error("stale connection revision"); + notifyMcpAuthorityChanged(); return rows[0]; }, /** Only a verified credential exchange may call this method. Refresh does not call it. */ @@ -70,6 +72,7 @@ export function makeMcpRepository(db = getDb()) { ) .returning(); if (!rows[0]) throw new Error("stale authorization generation"); + notifyMcpAuthorityChanged(); return rows[0]; }, async disconnect(id: string, expectedGeneration: number, now: Date) { @@ -92,10 +95,11 @@ export function makeMcpRepository(db = getDb()) { ) .returning(); if (!rows[0]) throw new Error("stale authorization generation"); + notifyMcpAuthorityChanged(); return rows[0]; }, async setEnabled(id: string, expectedRevision: number, enabled: boolean, now: Date) { - return db.transaction(async (tx) => { + const result = await db.transaction(async (tx) => { const [row] = await tx .select() .from(connection) @@ -131,6 +135,8 @@ export function makeMcpRepository(db = getDb()) { .returning(); return updated!; }); + notifyMcpAuthorityChanged(); + return result; }, async saveDraft( id: string, @@ -208,7 +214,7 @@ export function makeMcpRepository(db = getDb()) { now: Date, ) { const tools = reviewCatalog(input); - return db.transaction(async (tx) => { + const result = await db.transaction(async (tx) => { const [row] = await tx .select() .from(connection) @@ -266,6 +272,8 @@ export function makeMcpRepository(db = getDb()) { .where(eq(connection.id, id)); return published!; }); + notifyMcpAuthorityChanged(); + return result; }, async getCatalog(id: string, revision: number) { return ( diff --git a/orchestrator/src/mcp/review-state.ts b/orchestrator/src/mcp/review-state.ts index e7a618048..087c283de 100644 --- a/orchestrator/src/mcp/review-state.ts +++ b/orchestrator/src/mcp/review-state.ts @@ -1,6 +1,6 @@ import { create, toBinary, toJson, type MessageInitShape } from "@bufbuild/protobuf"; import { GetSessionMcpStateResponseSchema } from "../gen/engram/app/v1/integration_pb.ts"; -import type { McpJournal } from "./journal.ts"; +import { unknownContext, type McpJournal } from "./journal.ts"; import { reviewContext, policyReviewCurrent } from "./journal-review.ts"; import { restoreBindingSnapshot } from "./binding-store.ts"; import type { SessionBindingResolver } from "./session-resolver.ts"; @@ -75,10 +75,15 @@ export async function sessionMcpState(input: { (input.afterBindingToolName?.length ?? 0) > 128 ) throw new Error("invalid binding page"); - const data = await input.journal.readSession(input.sessionId, input.actorId, { - after: input.after, - limit: input.limit, - }); + const data = await input.journal.readSession( + input.sessionId, + input.actorId, + { + after: input.after, + limit: input.limit, + }, + input.now, + ); const response: MessageInitShape = { executions: [], bindingTools: [], @@ -128,7 +133,7 @@ export async function sessionMcpState(input: { response.bindingReceipt.allowedActions = ["rebind"]; let last = input.after ?? ""; for (const row of input.omitExecutions ? [] : data.rows) { - const { call, approval, resolution } = row; + const { call, approval, resolution, cancellation } = row; const actions: string[] = []; const review = approval && call.requestContext && call.originalArguments !== null @@ -183,6 +188,17 @@ export async function sessionMcpState(input: { : {}; const entry = { ...content, + expectedUnknownContext: + call.state === "outcome_unknown" && call.intentId && !resolution + ? unknownContext(call) + : undefined, + cancellationReceipt: cancellation + ? { + ...cancellation, + actorId: cancellation.actorId ?? "", + createdAt: cancellation.createdAt.toISOString(), + } + : undefined, bindingHash: call.bindingHash, executionId: call.id, connectionId: call.connectionId, @@ -214,6 +230,7 @@ export async function sessionMcpState(input: { actorId: resolution.actorId, outcome: resolution.outcome, createdAt: resolution.createdAt.toISOString(), + expectedContext: resolution.expectedContext ?? undefined, }, ] : []), diff --git a/orchestrator/src/mcp/runtime-authority.ts b/orchestrator/src/mcp/runtime-authority.ts index d4b344e46..3c34ae053 100644 --- a/orchestrator/src/mcp/runtime-authority.ts +++ b/orchestrator/src/mcp/runtime-authority.ts @@ -1,3 +1,4 @@ +import { McpAuthorityStale } from "./authority-error.ts"; import { subject } from "@casl/ability"; import { eq } from "drizzle-orm"; import { automation, automationRun, automationSession, mcpExecution } from "../db/schema.ts"; @@ -23,7 +24,7 @@ export async function lockRuntimeAuthority( .select() .from(automationSession) .where(eq(automationSession.sessionId, call.sessionId)); - if (!linked || !id) throw new Error("automation identity unavailable"); + if (!linked || !id) throw new McpAuthorityStale("automation identity unavailable"); const [owner] = await tx.select().from(automation).where(eq(automation.id, id)).for("share"); const [run] = await tx .select() @@ -47,7 +48,7 @@ export async function lockRuntimeAuthority( (session.role === "primary" && (run.sessionId !== call.sessionId || run.taskId !== call.taskId)) ) - throw new Error("automation authority unavailable"); + throw new McpAuthorityStale("automation authority unavailable"); authority = await lockSessionChain(tx, call.sessionId); if (call.classification === "write") { const resolved = call.requestContext?.resolvedTool; @@ -62,7 +63,7 @@ export async function lockRuntimeAuthority( publication.grantsHash !== authority.session.mcpBindingEnvelope?.profileGrantsHash || canonicalDigest(resolved.effectiveGrant) !== call.automationGrantHash ) - throw new Error("explicit published automation write grant required"); + throw new McpAuthorityStale("explicit published automation write grant required"); } } else { const human = await lockHumanSession(tx, call.sessionId, call.principalId, true); @@ -73,7 +74,7 @@ export async function lockRuntimeAuthority( !ability.can("prompt", subject("Session", { ...owner })) || call.authorizationKind === "automation" ) - throw new Error("current session permission required"); + throw new McpAuthorityStale("current session permission required"); authority = human; } if ( @@ -82,7 +83,7 @@ export async function lockRuntimeAuthority( authority.session.taskId !== call.taskId || ["done", "failed", "cancelled"].includes(authority.owner.status) ) - throw new Error("current session principal required"); + throw new McpAuthorityStale("current session principal required"); await assertLocalReviewCurrent(tx, call, authority.session, authority.ancestors, connectionLock); return authority; } diff --git a/orchestrator/src/mcp/session-bindings.ts b/orchestrator/src/mcp/session-bindings.ts index df584c867..86ec1e832 100644 --- a/orchestrator/src/mcp/session-bindings.ts +++ b/orchestrator/src/mcp/session-bindings.ts @@ -111,7 +111,7 @@ function policyFor(config: ReturnType, live: McpLiveAu throw new Error("MCP authority is not operator approved"); return policy; } -function managedAuthorityHash(live: McpLiveAuthority) { +export function managedAuthorityHash(live: McpLiveAuthority) { return canonicalDigest( live.connections .map((connection) => { diff --git a/orchestrator/src/mcp/session-resolver.ts b/orchestrator/src/mcp/session-resolver.ts index d5fa633c7..559ffa1ac 100644 --- a/orchestrator/src/mcp/session-resolver.ts +++ b/orchestrator/src/mcp/session-resolver.ts @@ -1,3 +1,4 @@ +import { McpAuthorityStale } from "./authority-error.ts"; import type { McpBoundTool } from "./types.ts"; import { checkBindingCurrent, @@ -35,7 +36,7 @@ export function assertManagedDenyAck(expected: ManagedDenyAck, actual: ManagedDe (key) => expected[key as keyof ManagedDenyAck] !== actual[key as keyof ManagedDenyAck], ) ) - throw new Error("MCP managed deny enforcement changed"); + throw new McpAuthorityStale("MCP managed deny enforcement changed"); } export interface BoundSessionAuthority { snapshot: McpSessionBindingSnapshot; @@ -84,16 +85,16 @@ export function resolveSnapshotTool( export function makeSessionBindingResolver(deps: SessionBindingResolverDeps) { async function readCurrent(sessionId: string): Promise { const leaf = await deps.readSession(sessionId); - if (!leaf?.active) throw new Error("MCP session unavailable"); + if (!leaf?.active) throw new McpAuthorityStale("MCP session unavailable"); const refs = leaf.snapshot.ancestorAuthorityRefs; - if (refs.length >= 32) throw new Error("MCP ancestry unavailable"); + if (refs.length >= 32) throw new McpAuthorityStale("MCP ancestry unavailable"); const seen = new Set(); let id: string | null = sessionId; for (let index = 0; index <= refs.length; index++) { - if (!id || seen.has(id)) throw new Error("MCP ancestry unavailable"); + if (!id || seen.has(id)) throw new McpAuthorityStale("MCP ancestry unavailable"); seen.add(id); const row: BoundSessionAuthority | null = index === 0 ? leaf : await deps.readSession(id); - if (!row?.active) throw new Error("MCP ancestor unavailable"); + if (!row?.active) throw new McpAuthorityStale("MCP ancestor unavailable"); const snapshot = row.snapshot; const expected = index === 0 ? undefined : refs[index - 1]; if ( @@ -102,19 +103,19 @@ export function makeSessionBindingResolver(deps: SessionBindingResolverDeps) { expected.bindingHash !== snapshot.bindingHash || expected.bindingEpoch !== snapshot.bindingEpoch) ) - throw new Error("MCP ancestor binding changed"); + throw new McpAuthorityStale("MCP ancestor binding changed"); const suffix = refs.slice(index); if (JSON.stringify(snapshot.ancestorAuthorityRefs) !== JSON.stringify(suffix)) - throw new Error("MCP ancestry changed"); + throw new McpAuthorityStale("MCP ancestry changed"); const live = await deps.readAuthority(snapshot.profileId); if ( !checkBindingCurrent({ snapshot, liveAuthority: live }).current || (await deps.readBindingEpoch(id)) !== snapshot.bindingEpoch ) - throw new Error("MCP authority changed"); + throw new McpAuthorityStale("MCP authority changed"); id = row.parentSessionId; } - if (id !== null) throw new Error("MCP ancestry truncated"); + if (id !== null) throw new McpAuthorityStale("MCP ancestry truncated"); if (!deps.enforceManagedDeny) throw new Error("MCP managed deny enforcement unavailable"); const expected = expectedManagedDenyAck(sessionId, leaf.snapshot); assertManagedDenyAck( diff --git a/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts b/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts index ee8803835..fba6f9688 100644 --- a/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts +++ b/web/src/gen/engram/app/v1/integration-IntegrationService_connectquery.ts @@ -181,6 +181,11 @@ export const cancelMcpCall = IntegrationService.method.cancelMcpCall; */ export const resolveMcpOutcome = IntegrationService.method.resolveMcpOutcome; +/** + * @generated from rpc engram.app.v1.IntegrationService.PrepareMcpRebind + */ +export const prepareMcpRebind = IntegrationService.method.prepareMcpRebind; + /** * @generated from rpc engram.app.v1.IntegrationService.RebindMcp */ diff --git a/web/src/gen/engram/app/v1/integration_pb.ts b/web/src/gen/engram/app/v1/integration_pb.ts index d0c59bd2b..3b03a81cb 100644 --- a/web/src/gen/engram/app/v1/integration_pb.ts +++ b/web/src/gen/engram/app/v1/integration_pb.ts @@ -2,8 +2,8 @@ // @generated from file engram/app/v1/integration.proto (package engram.app.v1, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; -import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; import type { GetMcpWritePolicyResponse } from "./session_pb"; import { file_engram_app_v1_session } from "./session_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/integration.proto. */ export const file_engram_app_v1_integration: GenFile = /*@__PURE__*/ - fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIo8DCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkSGgoSb3JpZ2luYWxfYXJndW1lbnRzGAogASgJEi0KDXJldmlld2VkX3Rvb2wYCyABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgMIAMoCRJIChdleHBlY3RlZF9yZXZpZXdfY29udGV4dBgNIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IkkKF0xpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSGgoSYWZ0ZXJfZXhlY3V0aW9uX2lkGAIgASgJImQKGExpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRItCglhcHByb3ZhbHMYASADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhkKEW5leHRfZXhlY3V0aW9uX2lkGAIgASgJIosBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgSSAoXZXhwZWN0ZWRfcmV2aWV3X2NvbnRleHQYAyABKAsyJy5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dCJeChlEZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJEjIKB3JlY2VpcHQYAiABKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmVjZWlwdCIsChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkiJgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJIkEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCSIsChlSZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEg8KB291dGNvbWUYASABKAkiRQoQUmViaW5kTWNwUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSIpChFSZWJpbmRNY3BSZXNwb25zZRIUCgxiaW5kaW5nX2hhc2gYASABKAkilQEKF1B1dE1jcENyZWRlbnRpYWxSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAiABKA0SGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgDIAEoDRIMCgRraW5kGAQgASgJEg4KBmhlYWRlchgFIAEoCRINCgV2YWx1ZRgGIAEoCSKUAQoTTWNwQ3JlZGVudGlhbFN0YXR1cxIOCgZzdGF0dXMYASABKAkSEQoJcmVmZXJlbmNlGAIgASgJEgwKBGtpbmQYAyABKAkSGwoTY3JlZGVudGlhbF9pZGVudGl0eRgEIAEoCRIPCgd2ZXJzaW9uGAUgASgDEh4KFnJlc2VydmF0aW9uX2dlbmVyYXRpb24YBiABKAMijAEKGFB1dE1jcENyZWRlbnRpYWxSZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyKUAQogQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMikAEKHFJlY292ZXJNY3BDcmVkZW50aWFsUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMihgEKEk1jcENyZWRlbnRpYWxTdGF0ZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyI7ChZNY3BBcHByb3ZlZERlc3RpbmF0aW9uEg8KB3pvbmVfaWQYASABKAkSEAoIZW5kcG9pbnQYAiABKAkiuwEKFU1jcENsaWVudFJlZ2lzdHJhdGlvbhIKCgJpZBgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSFAoMY29uZmlkZW50aWFsGAMgASgIEhQKDHJlZGlyZWN0X3VyaRgEIAEoCRIOCgZpc3N1ZXIYBSABKAkSNwoIcmVzb3VyY2UYBiABKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SDgoGc2NvcGVzGAcgAygJIisKEkdldE1jcFNldHVwUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJIrwCChNHZXRNY3BTZXR1cFJlc3BvbnNlEg8KB2VuYWJsZWQYASABKAgSOwoNcmVnaXN0cmF0aW9ucxgCIAMoCzIkLmVuZ3JhbS5hcHAudjEuTWNwQ2xpZW50UmVnaXN0cmF0aW9uEjsKDGRlc3RpbmF0aW9ucxgDIAMoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92ZWREZXN0aW5hdGlvbhIyCgdjdXJyZW50GAQgASgLMiEuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdGUSGgoSdHJhbnNpdGlvbl9wZW5kaW5nGAUgASgIEg8KB2Zsb3dfaWQYBiABKAkSFwoPZmxvd19leHBpcmVzX2F0GAcgASgJEiAKGGNsaWVudF9zZWNyZXRfY29uZmlndXJlZBgIIAEoCCLUAQofQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDwoHZmxvd19pZBgEIAEoCRINCgVzdGF0ZRgFIAEoCRIMCgRjb2RlGAYgASgJEhMKBmlzc3VlchgHIAEoCUgAiAEBEhQKDHJlZGlyZWN0X3VyaRgIIAEoCUIJCgdfaXNzdWVyImwKG1JlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0i6gMKGE1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhYKDnJlcXVlc3RfZGlnZXN0GAUgASgJEhUKDWFyZ3VtZW50X2hhc2gYBiABKAkSFAoMYmluZGluZ19oYXNoGAcgASgJEhUKDWNvbm5lY3Rpb25faWQYCCABKAkSEgoKY2F0YWxvZ19pZBgJIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGAogASgNEhcKD2NvbmZpZ19yZXZpc2lvbhgLIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDCABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgNIAEoCRIVCg11cHN0cmVhbV9uYW1lGA4gASgJEhgKEHRvb2xfZmluZ2VycHJpbnQYDyABKAkSFgoOY29udGV4dF9kaWdlc3QYECABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgRIAEoDRISCgpleHBpcmVzX2F0GBIgASgJEhcKD3BvbGljeV9yZXZpc2lvbhgTIAEoCRIVCg1iaW5kaW5nX2Vwb2NoGBQgASgJIr8BChJNY3BBcHByb3ZhbFJlY2VpcHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhAKCGRlY2lzaW9uGAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKCmRlY2lkZWRfYXQYBCABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgFIAEoDRJBChByZXZpZXdlZF9jb250ZXh0GAYgASgLMicuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbFJldmlld0NvbnRleHQi2AMKEk1jcEV4ZWN1dGlvblJldmlldxIUCgxleGVjdXRpb25faWQYASABKAkSFQoNY29ubmVjdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRIYChBqb3VybmFsX3JldmlzaW9uGAQgASgNEhoKEnJlc3VsdF91bmF2YWlsYWJsZRgFIAEoCBIqCgZyZXZpZXcYBiABKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhcKD2FsbG93ZWRfYWN0aW9ucxgHIAMoCRIOCgZyZWFzb24YCCABKAkSFAoMdG9vbF9jYWxsX2lkGAkgASgJEg4KBnJ1bl9pZBgKIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgLIAEoCRIVCg11cHN0cmVhbV9uYW1lGAwgASgJEhkKEWFwcHJvdmFsX2RlY2lzaW9uGA0gASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgOIAEoCRItCg1yZXZpZXdlZF90b29sGA8gASgLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YECADKAkSFAoMYmluZGluZ19oYXNoGBEgASgJImMKFE1jcFJlc29sdXRpb25SZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRIPCgdvdXRjb21lGAMgASgJEhIKCmNyZWF0ZWRfYXQYBCABKAkigQEKEU1jcEJpbmRpbmdSZWNlaXB0EhQKDGJpbmRpbmdfaGFzaBgBIAEoCRIVCg1iaW5kaW5nX2Vwb2NoGAIgASgJEg8KB2N1cnJlbnQYAyABKAgSFQoNc3RhbGVfcmVhc29ucxgEIAMoCRIXCg9hbGxvd2VkX2FjdGlvbnMYBSADKAkigAIKGUdldFNlc3Npb25NY3BTdGF0ZVJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIaChJhZnRlcl9leGVjdXRpb25faWQYAiABKAkSEQoJcGFnZV9zaXplGAMgASgNEh4KEXBsYW5fdG9vbF9jYWxsX2lkGAQgASgJSACIAQESHwoXYWZ0ZXJfYmluZGluZ190b29sX25hbWUYBSABKAkSGQoRYmluZGluZ19wYWdlX3NpemUYBiABKA0SFQoNb21pdF9iaW5kaW5ncxgHIAEoCBIXCg9vbWl0X2V4ZWN1dGlvbnMYCCABKAhCFAoSX3BsYW5fdG9vbF9jYWxsX2lkIr8EChpHZXRTZXNzaW9uTWNwU3RhdGVSZXNwb25zZRI5Cg9iaW5kaW5nX3JlY2VpcHQYASABKAsyIC5lbmdyYW0uYXBwLnYxLk1jcEJpbmRpbmdSZWNlaXB0EjUKCmV4ZWN1dGlvbnMYAiADKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEV4ZWN1dGlvblJldmlldxItCglhcHByb3ZhbHMYAyADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhwKFHVua25vd25fd3JpdGVfYmxvY2tzGAQgAygJEkAKE3Jlc29sdXRpb25fcmVjZWlwdHMYBSADKAsyIy5lbmdyYW0uYXBwLnYxLk1jcFJlc29sdXRpb25SZWNlaXB0EhcKD2FsbG93ZWRfYWN0aW9ucxgGIAMoCRIUCgxhdmFpbGFiaWxpdHkYByABKAkSGQoRbmV4dF9leGVjdXRpb25faWQYCCABKAkSPgoMd3JpdGVfcG9saWN5GAkgASgLMiguZW5ncmFtLmFwcC52MS5HZXRNY3BXcml0ZVBvbGljeVJlc3BvbnNlEjkKDWJpbmRpbmdfdG9vbHMYCiADKAsyIi5lbmdyYW0uYXBwLnYxLk1jcEJvdW5kVG9vbFN1bW1hcnkSHgoWbmV4dF9iaW5kaW5nX3Rvb2xfbmFtZRgLIAEoCRIeChZoYXNfbW9yZV9iaW5kaW5nX3Rvb2xzGAwgASgIEhsKE2hhc19tb3JlX2V4ZWN1dGlvbnMYDSABKAgiygIKE01jcEJvdW5kVG9vbFN1bW1hcnkSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIYChBjb25uZWN0aW9uX2FsaWFzGAIgASgJEhcKD21vZGVsX3Rvb2xfbmFtZRgDIAEoCRItCg1yZXZpZXdlZF90b29sGAQgASgLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YBSADKAkSGAoQYXV0b21hdGlvbl93cml0ZRgGIAEoCBIUCgx1bnJlc3RyaWN0ZWQYByABKAgSEgoKY2F0YWxvZ19pZBgIIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGAkgASgNEhcKD2NvbmZpZ19yZXZpc2lvbhgKIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YCyABKA0y8BYKEkludGVncmF0aW9uU2VydmljZRJdCg5MaXN0Q29ubmVjdG9ycxIkLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEmAKD1Vwc2VydENvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuVXBzZXJ0Q29ubmVjdG9yUmVzcG9uc2USYAoPRGVsZXRlQ29ubmVjdG9yEiUuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0b3JSZXNwb25zZRJyChVHZXRJbnRlZ3JhdGlvbkNhdGFsb2cSKy5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QaLC5lbmdyYW0uYXBwLnYxLkdldEludGVncmF0aW9uQ2F0YWxvZ1Jlc3BvbnNlEmYKEVNldE1pbnRDcmVkZW50aWFsEicuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLlNldE1pbnRDcmVkZW50aWFsUmVzcG9uc2USbAoTVXBsb2FkQ29ubmVjdG9yTG9nbxIpLmVuZ3JhbS5hcHAudjEuVXBsb2FkQ29ubmVjdG9yTG9nb1JlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXNwb25zZRJaCg1UZXN0Q29ubmVjdG9yEiMuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdG9yUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlc3BvbnNlEm8KFFNldENvbm5lY3RvclNldHRpbmdzEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1JlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USYAoPTGlzdENvbm5lY3Rpb25zEiUuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXF1ZXN0GiYuZW5ncmFtLmFwcC52MS5MaXN0Q29ubmVjdGlvbnNSZXNwb25zZRJjChBDcmVhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5DcmVhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEFVwZGF0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLlVwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USYwoQRGVsZXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXNwb25zZRJdCg5UZXN0Q29ubmVjdGlvbhIkLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXF1ZXN0GiUuZW5ncmFtLmFwcC52MS5UZXN0Q29ubmVjdGlvblJlc3BvbnNlEm8KFFNldENvbm5lY3Rpb25FbmFibGVkEiouZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlNldENvbm5lY3Rpb25FbmFibGVkUmVzcG9uc2USbAoTR2V0R29vZ2xlQ2xvdWRTZXR1cBIpLmVuZ3JhbS5hcHAudjEuR2V0R29vZ2xlQ2xvdWRTZXR1cFJlcXVlc3QaKi5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USYwoQUHV0TWNwQ3JlZGVudGlhbBImLmVuZ3JhbS5hcHAudjEuUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLlB1dE1jcENyZWRlbnRpYWxSZXNwb25zZRJUCgtHZXRNY3BTZXR1cBIhLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRNY3BTZXR1cFJlc3BvbnNlEnsKGENvbXBsZXRlTWNwQXV0aG9yaXphdGlvbhIuLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBovLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVzcG9uc2USbwoUUmVjb3Zlck1jcENyZWRlbnRpYWwSKi5lbmdyYW0uYXBwLnYxLlJlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuUmVjb3Zlck1jcENyZWRlbnRpYWxSZXNwb25zZRJXCgxBdXRob3JpemVNY3ASIi5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlc3BvbnNlEloKDURpc2Nvbm5lY3RNY3ASIy5lbmdyYW0uYXBwLnYxLkRpc2Nvbm5lY3RNY3BSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVzcG9uc2USaQoSR2V0U2Vzc2lvbk1jcFN0YXRlEiguZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXF1ZXN0GikuZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXNwb25zZRJjChBMaXN0TWNwQXBwcm92YWxzEiYuZW5ncmFtLmFwcC52MS5MaXN0TWNwQXBwcm92YWxzUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1Jlc3BvbnNlEmYKEURlY2lkZU1jcEFwcHJvdmFsEicuZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLkRlY2lkZU1jcEFwcHJvdmFsUmVzcG9uc2USWgoNQ2FuY2VsTWNwQ2FsbBIjLmVuZ3JhbS5hcHAudjEuQ2FuY2VsTWNwQ2FsbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXNwb25zZRJmChFSZXNvbHZlTWNwT3V0Y29tZRInLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5SZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEk4KCVJlYmluZE1jcBIfLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVzcG9uc2ViBnByb3RvMw", [file_engram_app_v1_session]); + fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIo8DCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkSGgoSb3JpZ2luYWxfYXJndW1lbnRzGAogASgJEi0KDXJldmlld2VkX3Rvb2wYCyABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgMIAMoCRJIChdleHBlY3RlZF9yZXZpZXdfY29udGV4dBgNIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IkkKF0xpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSGgoSYWZ0ZXJfZXhlY3V0aW9uX2lkGAIgASgJImQKGExpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRItCglhcHByb3ZhbHMYASADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhkKEW5leHRfZXhlY3V0aW9uX2lkGAIgASgJIosBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgSSAoXZXhwZWN0ZWRfcmV2aWV3X2NvbnRleHQYAyABKAsyJy5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dCJeChlEZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJEjIKB3JlY2VpcHQYAiABKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmVjZWlwdCJPChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSIQoZZXhwZWN0ZWRfam91cm5hbF9yZXZpc2lvbhgCIAEoDSJeChVDYW5jZWxNY3BDYWxsUmVzcG9uc2USDQoFc3RhdGUYASABKAkSNgoHcmVjZWlwdBgCIAEoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQ2FuY2VsbGF0aW9uUmVjZWlwdCKFAQoYUmVzb2x2ZU1jcE91dGNvbWVSZXF1ZXN0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIPCgdvdXRjb21lGAIgASgJEkIKGGV4cGVjdGVkX3Vua25vd25fY29udGV4dBgDIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwVW5rbm93bkNvbnRleHQiYgoZUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRIPCgdvdXRjb21lGAEgASgJEjQKB3JlY2VpcHQYAiABKAsyIy5lbmdyYW0uYXBwLnYxLk1jcFJlc29sdXRpb25SZWNlaXB0IkwKF1ByZXBhcmVNY3BSZWJpbmRSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSHQoVZXhwZWN0ZWRfYmluZGluZ19oYXNoGAIgASgJIqkCChhQcmVwYXJlTWNwUmViaW5kUmVzcG9uc2USFAoMYmluZGluZ19oYXNoGAEgASgJEhUKDWJpbmRpbmdfZXBvY2gYAiABKAkSOQoNYmluZGluZ190b29scxgDIAMoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQm91bmRUb29sU3VtbWFyeRISCgpkZW55X2hvc3RzGAQgAygJEhUKDWRlbnlfcmV2aXNpb24YBSABKAkSEwoLZGVueV9kaWdlc3QYBiABKAkSNwoLY29ubmVjdGlvbnMYByADKAsyIi5lbmdyYW0uYXBwLnYxLk1jcFJlYmluZENvbm5lY3Rpb24SLAoFZHJhaW4YCCABKAsyHS5lbmdyYW0uYXBwLnYxLk1jcFJlYmluZERyYWluIloKDk1jcFJlYmluZERyYWluEjMKBnJlYXNvbhgBIAEoDjIjLmVuZ3JhbS5hcHAudjEuTWNwUmViaW5kRHJhaW5SZWFzb24SEwoLcmV0cnlfYWZ0ZXIYAiABKAkixAEKE01jcFJlYmluZENvbm5lY3Rpb24SFQoNY29ubmVjdGlvbl9pZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRIMCgR6b25lGAQgASgJEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYBSABKAkSFwoPY29uZmlnX3JldmlzaW9uGAYgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgHIAEoDRIYChBjYXRhbG9nX3JldmlzaW9uGAggASgNImYKEFJlYmluZE1jcFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIdChVleHBlY3RlZF9iaW5kaW5nX2hhc2gYAiABKAkSHwoXZXhwZWN0ZWRfY2FuZGlkYXRlX2hhc2gYAyABKAkiKQoRUmViaW5kTWNwUmVzcG9uc2USFAoMYmluZGluZ19oYXNoGAEgASgJIpUBChdQdXRNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDAoEa2luZBgEIAEoCRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkilAEKE01jcENyZWRlbnRpYWxTdGF0dXMSDgoGc3RhdHVzGAEgASgJEhEKCXJlZmVyZW5jZRgCIAEoCRIMCgRraW5kGAMgASgJEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYBCABKAkSDwoHdmVyc2lvbhgFIAEoAxIeChZyZXNlcnZhdGlvbl9nZW5lcmF0aW9uGAYgASgDIowBChhQdXRNY3BDcmVkZW50aWFsUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMilAEKIENvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbhI2CgpjcmVkZW50aWFsGAIgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdHVzIpABChxSZWNvdmVyTWNwQ3JlZGVudGlhbFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbhI2CgpjcmVkZW50aWFsGAIgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdHVzIoYBChJNY3BDcmVkZW50aWFsU3RhdGUSOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMiOwoWTWNwQXBwcm92ZWREZXN0aW5hdGlvbhIPCgd6b25lX2lkGAEgASgJEhAKCGVuZHBvaW50GAIgASgJIrsBChVNY3BDbGllbnRSZWdpc3RyYXRpb24SCgoCaWQYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhQKDGNvbmZpZGVudGlhbBgDIAEoCBIUCgxyZWRpcmVjdF91cmkYBCABKAkSDgoGaXNzdWVyGAUgASgJEjcKCHJlc291cmNlGAYgASgLMiUuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZlZERlc3RpbmF0aW9uEg4KBnNjb3BlcxgHIAMoCSIrChJHZXRNY3BTZXR1cFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCSK8AgoTR2V0TWNwU2V0dXBSZXNwb25zZRIPCgdlbmFibGVkGAEgASgIEjsKDXJlZ2lzdHJhdGlvbnMYAiADKAsyJC5lbmdyYW0uYXBwLnYxLk1jcENsaWVudFJlZ2lzdHJhdGlvbhI7CgxkZXN0aW5hdGlvbnMYAyADKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SMgoHY3VycmVudBgEIAEoCzIhLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXRlEhoKEnRyYW5zaXRpb25fcGVuZGluZxgFIAEoCBIPCgdmbG93X2lkGAYgASgJEhcKD2Zsb3dfZXhwaXJlc19hdBgHIAEoCRIgChhjbGllbnRfc2VjcmV0X2NvbmZpZ3VyZWQYCCABKAgi1AEKH0NvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAMgASgNEg8KB2Zsb3dfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSDAoEY29kZRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIUCgxyZWRpcmVjdF91cmkYCCABKAlCCQoHX2lzc3VlciJsChtSZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAMgASgNIuoDChhNY3BBcHByb3ZhbFJldmlld0NvbnRleHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgFIAEoCRIVCg1hcmd1bWVudF9oYXNoGAYgASgJEhQKDGJpbmRpbmdfaGFzaBgHIAEoCRIVCg1jb25uZWN0aW9uX2lkGAggASgJEhIKCmNhdGFsb2dfaWQYCSABKAkSGAoQY2F0YWxvZ19yZXZpc2lvbhgKIAEoDRIXCg9jb25maWdfcmV2aXNpb24YCyABKA0SFwoPYXV0aF9nZW5lcmF0aW9uGAwgASgNEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYDSABKAkSFQoNdXBzdHJlYW1fbmFtZRgOIAEoCRIYChB0b29sX2ZpbmdlcnByaW50GA8gASgJEhYKDmNvbnRleHRfZGlnZXN0GBAgASgJEhgKEGpvdXJuYWxfcmV2aXNpb24YESABKA0SEgoKZXhwaXJlc19hdBgSIAEoCRIXCg9wb2xpY3lfcmV2aXNpb24YEyABKAkSFQoNYmluZGluZ19lcG9jaBgUIAEoCSK/AQoSTWNwQXBwcm92YWxSZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghkZWNpc2lvbhgCIAEoCRIQCghhY3Rvcl9pZBgDIAEoCRISCgpkZWNpZGVkX2F0GAQgASgJEhgKEGpvdXJuYWxfcmV2aXNpb24YBSABKA0SQQoQcmV2aWV3ZWRfY29udGV4dBgGIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IuEEChJNY3BFeGVjdXRpb25SZXZpZXcSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhUKDWNvbm5lY3Rpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgEIAEoDRIaChJyZXN1bHRfdW5hdmFpbGFibGUYBSABKAgSKgoGcmV2aWV3GAYgASgLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbBIXCg9hbGxvd2VkX2FjdGlvbnMYByADKAkSDgoGcmVhc29uGAggASgJEhQKDHRvb2xfY2FsbF9pZBgJIAEoCRIOCgZydW5faWQYCiABKAkSFgoOcmVxdWVzdF9kaWdlc3QYCyABKAkSFQoNdXBzdHJlYW1fbmFtZRgMIAEoCRIZChFhcHByb3ZhbF9kZWNpc2lvbhgNIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYDiABKAkSLQoNcmV2aWV3ZWRfdG9vbBgPIAEoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBIqCiJhcmd1bWVudF9jb25zdHJhaW50X2RvY3VtZW50c19qc29uGBAgAygJEhQKDGJpbmRpbmdfaGFzaBgRIAEoCRJCChhleHBlY3RlZF91bmtub3duX2NvbnRleHQYEiABKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFVua25vd25Db250ZXh0EkMKFGNhbmNlbGxhdGlvbl9yZWNlaXB0GBMgASgLMiUuZW5ncmFtLmFwcC52MS5NY3BDYW5jZWxsYXRpb25SZWNlaXB0IoQBChFNY3BVbmtub3duQ29udGV4dBIUCgxleGVjdXRpb25faWQYASABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgCIAEoDRIWCg5yZXF1ZXN0X2RpZ2VzdBgDIAEoCRIUCgxiaW5kaW5nX2hhc2gYBCABKAkSEQoJaW50ZW50X2lkGAUgASgJIq8BChZNY3BDYW5jZWxsYXRpb25SZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRINCgVjYXVzZRgDIAEoCRIhChlleHBlY3RlZF9qb3VybmFsX3JldmlzaW9uGAQgASgNEhgKEGpvdXJuYWxfcmV2aXNpb24YBSABKA0SDQoFc3RhdGUYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCSKfAQoUTWNwUmVzb2x1dGlvblJlY2VpcHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEg8KB291dGNvbWUYAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoCRI6ChBleHBlY3RlZF9jb250ZXh0GAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BVbmtub3duQ29udGV4dCKBAQoRTWNwQmluZGluZ1JlY2VpcHQSFAoMYmluZGluZ19oYXNoGAEgASgJEhUKDWJpbmRpbmdfZXBvY2gYAiABKAkSDwoHY3VycmVudBgDIAEoCBIVCg1zdGFsZV9yZWFzb25zGAQgAygJEhcKD2FsbG93ZWRfYWN0aW9ucxgFIAMoCSKAAgoZR2V0U2Vzc2lvbk1jcFN0YXRlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhoKEmFmdGVyX2V4ZWN1dGlvbl9pZBgCIAEoCRIRCglwYWdlX3NpemUYAyABKA0SHgoRcGxhbl90b29sX2NhbGxfaWQYBCABKAlIAIgBARIfChdhZnRlcl9iaW5kaW5nX3Rvb2xfbmFtZRgFIAEoCRIZChFiaW5kaW5nX3BhZ2Vfc2l6ZRgGIAEoDRIVCg1vbWl0X2JpbmRpbmdzGAcgASgIEhcKD29taXRfZXhlY3V0aW9ucxgIIAEoCEIUChJfcGxhbl90b29sX2NhbGxfaWQivwQKGkdldFNlc3Npb25NY3BTdGF0ZVJlc3BvbnNlEjkKD2JpbmRpbmdfcmVjZWlwdBgBIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwQmluZGluZ1JlY2VpcHQSNQoKZXhlY3V0aW9ucxgCIAMoCzIhLmVuZ3JhbS5hcHAudjEuTWNwRXhlY3V0aW9uUmV2aWV3Ei0KCWFwcHJvdmFscxgDIAMoCzIaLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWwSHAoUdW5rbm93bl93cml0ZV9ibG9ja3MYBCADKAkSQAoTcmVzb2x1dGlvbl9yZWNlaXB0cxgFIAMoCzIjLmVuZ3JhbS5hcHAudjEuTWNwUmVzb2x1dGlvblJlY2VpcHQSFwoPYWxsb3dlZF9hY3Rpb25zGAYgAygJEhQKDGF2YWlsYWJpbGl0eRgHIAEoCRIZChFuZXh0X2V4ZWN1dGlvbl9pZBgIIAEoCRI+Cgx3cml0ZV9wb2xpY3kYCSABKAsyKC5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVzcG9uc2USOQoNYmluZGluZ190b29scxgKIAMoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQm91bmRUb29sU3VtbWFyeRIeChZuZXh0X2JpbmRpbmdfdG9vbF9uYW1lGAsgASgJEh4KFmhhc19tb3JlX2JpbmRpbmdfdG9vbHMYDCABKAgSGwoTaGFzX21vcmVfZXhlY3V0aW9ucxgNIAEoCCLKAgoTTWNwQm91bmRUb29sU3VtbWFyeRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhgKEGNvbm5lY3Rpb25fYWxpYXMYAiABKAkSFwoPbW9kZWxfdG9vbF9uYW1lGAMgASgJEi0KDXJldmlld2VkX3Rvb2wYBCABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgFIAMoCRIYChBhdXRvbWF0aW9uX3dyaXRlGAYgASgIEhQKDHVucmVzdHJpY3RlZBgHIAEoCBISCgpjYXRhbG9nX2lkGAggASgJEhgKEGNhdGFsb2dfcmV2aXNpb24YCSABKA0SFwoPY29uZmlnX3JldmlzaW9uGAogASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgLIAEoDSrIAQoUTWNwUmViaW5kRHJhaW5SZWFzb24SJwojTUNQX1JFQklORF9EUkFJTl9SRUFTT05fVU5TUEVDSUZJRUQQABIwCixNQ1BfUkVCSU5EX0RSQUlOX1JFQVNPTl9ESVNQQVRDSF9JTl9QUk9HUkVTUxABEioKJk1DUF9SRUJJTkRfRFJBSU5fUkVBU09OX1VOS05PV05fSU5URU5UEAISKQolTUNQX1JFQklORF9EUkFJTl9SRUFTT05fTEVHQUNZX0lOVEVOVBADMtUXChJJbnRlZ3JhdGlvblNlcnZpY2USXQoOTGlzdENvbm5lY3RvcnMSJC5lbmdyYW0uYXBwLnYxLkxpc3RDb25uZWN0b3JzUmVxdWVzdBolLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXNwb25zZRJgCg9VcHNlcnRDb25uZWN0b3ISJS5lbmdyYW0uYXBwLnYxLlVwc2VydENvbm5lY3RvclJlcXVlc3QaJi5lbmdyYW0uYXBwLnYxLlVwc2VydENvbm5lY3RvclJlc3BvbnNlEmAKD0RlbGV0ZUNvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2UScgoVR2V0SW50ZWdyYXRpb25DYXRhbG9nEisuZW5ncmFtLmFwcC52MS5HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXF1ZXN0GiwuZW5ncmFtLmFwcC52MS5HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXNwb25zZRJmChFTZXRNaW50Q3JlZGVudGlhbBInLmVuZ3JhbS5hcHAudjEuU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEmwKE1VwbG9hZENvbm5lY3RvckxvZ28SKS5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXF1ZXN0GiouZW5ncmFtLmFwcC52MS5VcGxvYWRDb25uZWN0b3JMb2dvUmVzcG9uc2USWgoNVGVzdENvbm5lY3RvchIjLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXNwb25zZRJvChRTZXRDb25uZWN0b3JTZXR0aW5ncxIqLmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0GisuZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1Jlc3BvbnNlEmAKD0xpc3RDb25uZWN0aW9ucxIlLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3Rpb25zUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3Rpb25zUmVzcG9uc2USYwoQQ3JlYXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkNyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRJjChBVcGRhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuVXBkYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEERlbGV0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0aW9uUmVzcG9uc2USXQoOVGVzdENvbm5lY3Rpb24SJC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0aW9uUmVxdWVzdBolLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXNwb25zZRJvChRTZXRDb25uZWN0aW9uRW5hYmxlZBIqLmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdGlvbkVuYWJsZWRSZXF1ZXN0GisuZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEmwKE0dldEdvb2dsZUNsb3VkU2V0dXASKS5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXF1ZXN0GiouZW5ncmFtLmFwcC52MS5HZXRHb29nbGVDbG91ZFNldHVwUmVzcG9uc2USVAoLRGlzY292ZXJNY3ASIS5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyTWNwUmVxdWVzdBoiLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXNwb25zZRJRCgpQdWJsaXNoTWNwEiAuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVxdWVzdBohLmVuZ3JhbS5hcHAudjEuUHVibGlzaE1jcFJlc3BvbnNlEmMKEFB1dE1jcENyZWRlbnRpYWwSJi5lbmdyYW0uYXBwLnYxLlB1dE1jcENyZWRlbnRpYWxSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5QdXRNY3BDcmVkZW50aWFsUmVzcG9uc2USVAoLR2V0TWNwU2V0dXASIS5lbmdyYW0uYXBwLnYxLkdldE1jcFNldHVwUmVxdWVzdBoiLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXNwb25zZRJ7ChhDb21wbGV0ZU1jcEF1dGhvcml6YXRpb24SLi5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlcXVlc3QaLy5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlc3BvbnNlEm8KFFJlY292ZXJNY3BDcmVkZW50aWFsEiouZW5ncmFtLmFwcC52MS5SZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlJlY292ZXJNY3BDcmVkZW50aWFsUmVzcG9uc2USVwoMQXV0aG9yaXplTWNwEiIuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXNwb25zZRJaCg1EaXNjb25uZWN0TWNwEiMuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGlzY29ubmVjdE1jcFJlc3BvbnNlEmkKEkdldFNlc3Npb25NY3BTdGF0ZRIoLmVuZ3JhbS5hcHAudjEuR2V0U2Vzc2lvbk1jcFN0YXRlUmVxdWVzdBopLmVuZ3JhbS5hcHAudjEuR2V0U2Vzc2lvbk1jcFN0YXRlUmVzcG9uc2USYwoQTGlzdE1jcEFwcHJvdmFscxImLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRJmChFEZWNpZGVNY3BBcHByb3ZhbBInLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEloKDUNhbmNlbE1jcENhbGwSIy5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVzcG9uc2USZgoRUmVzb2x2ZU1jcE91dGNvbWUSJy5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRJjChBQcmVwYXJlTWNwUmViaW5kEiYuZW5ncmFtLmFwcC52MS5QcmVwYXJlTWNwUmViaW5kUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuUHJlcGFyZU1jcFJlYmluZFJlc3BvbnNlEk4KCVJlYmluZE1jcBIfLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVzcG9uc2ViBnByb3RvMw", [file_engram_app_v1_session]); /** * @generated from message engram.app.v1.GoogleCloudConnectionConfig @@ -1625,6 +1625,11 @@ export type CancelMcpCallRequest = Message<"engram.app.v1.CancelMcpCallRequest"> * @generated from field: string execution_id = 1; */ executionId: string; + + /** + * @generated from field: uint32 expected_journal_revision = 2; + */ + expectedJournalRevision: number; }; /** @@ -1642,6 +1647,11 @@ export type CancelMcpCallResponse = Message<"engram.app.v1.CancelMcpCallResponse * @generated from field: string state = 1; */ state: string; + + /** + * @generated from field: engram.app.v1.McpCancellationReceipt receipt = 2; + */ + receipt?: McpCancellationReceipt; }; /** @@ -1664,6 +1674,11 @@ export type ResolveMcpOutcomeRequest = Message<"engram.app.v1.ResolveMcpOutcomeR * @generated from field: string outcome = 2; */ outcome: string; + + /** + * @generated from field: engram.app.v1.McpUnknownContext expected_unknown_context = 3; + */ + expectedUnknownContext?: McpUnknownContext; }; /** @@ -1681,6 +1696,11 @@ export type ResolveMcpOutcomeResponse = Message<"engram.app.v1.ResolveMcpOutcome * @generated from field: string outcome = 1; */ outcome: string; + + /** + * @generated from field: engram.app.v1.McpResolutionReceipt receipt = 2; + */ + receipt?: McpResolutionReceipt; }; /** @@ -1690,6 +1710,154 @@ export type ResolveMcpOutcomeResponse = Message<"engram.app.v1.ResolveMcpOutcome export const ResolveMcpOutcomeResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_integration, 57); +/** + * @generated from message engram.app.v1.PrepareMcpRebindRequest + */ +export type PrepareMcpRebindRequest = Message<"engram.app.v1.PrepareMcpRebindRequest"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string expected_binding_hash = 2; + */ + expectedBindingHash: string; +}; + +/** + * Describes the message engram.app.v1.PrepareMcpRebindRequest. + * Use `create(PrepareMcpRebindRequestSchema)` to create a new message. + */ +export const PrepareMcpRebindRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 58); + +/** + * @generated from message engram.app.v1.PrepareMcpRebindResponse + */ +export type PrepareMcpRebindResponse = Message<"engram.app.v1.PrepareMcpRebindResponse"> & { + /** + * @generated from field: string binding_hash = 1; + */ + bindingHash: string; + + /** + * @generated from field: string binding_epoch = 2; + */ + bindingEpoch: string; + + /** + * @generated from field: repeated engram.app.v1.McpBoundToolSummary binding_tools = 3; + */ + bindingTools: McpBoundToolSummary[]; + + /** + * @generated from field: repeated string deny_hosts = 4; + */ + denyHosts: string[]; + + /** + * @generated from field: string deny_revision = 5; + */ + denyRevision: string; + + /** + * @generated from field: string deny_digest = 6; + */ + denyDigest: string; + + /** + * @generated from field: repeated engram.app.v1.McpRebindConnection connections = 7; + */ + connections: McpRebindConnection[]; + + /** + * @generated from field: engram.app.v1.McpRebindDrain drain = 8; + */ + drain?: McpRebindDrain; +}; + +/** + * Describes the message engram.app.v1.PrepareMcpRebindResponse. + * Use `create(PrepareMcpRebindResponseSchema)` to create a new message. + */ +export const PrepareMcpRebindResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 59); + +/** + * @generated from message engram.app.v1.McpRebindDrain + */ +export type McpRebindDrain = Message<"engram.app.v1.McpRebindDrain"> & { + /** + * @generated from field: engram.app.v1.McpRebindDrainReason reason = 1; + */ + reason: McpRebindDrainReason; + + /** + * @generated from field: string retry_after = 2; + */ + retryAfter: string; +}; + +/** + * Describes the message engram.app.v1.McpRebindDrain. + * Use `create(McpRebindDrainSchema)` to create a new message. + */ +export const McpRebindDrainSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 60); + +/** + * @generated from message engram.app.v1.McpRebindConnection + */ +export type McpRebindConnection = Message<"engram.app.v1.McpRebindConnection"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; + + /** + * @generated from field: string alias = 2; + */ + alias: string; + + /** + * @generated from field: string endpoint = 3; + */ + endpoint: string; + + /** + * @generated from field: string zone = 4; + */ + zone: string; + + /** + * @generated from field: string credential_identity = 5; + */ + credentialIdentity: string; + + /** + * @generated from field: uint32 config_revision = 6; + */ + configRevision: number; + + /** + * @generated from field: uint32 auth_generation = 7; + */ + authGeneration: number; + + /** + * @generated from field: uint32 catalog_revision = 8; + */ + catalogRevision: number; +}; + +/** + * Describes the message engram.app.v1.McpRebindConnection. + * Use `create(McpRebindConnectionSchema)` to create a new message. + */ +export const McpRebindConnectionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 61); + /** * @generated from message engram.app.v1.RebindMcpRequest */ @@ -1703,6 +1871,11 @@ export type RebindMcpRequest = Message<"engram.app.v1.RebindMcpRequest"> & { * @generated from field: string expected_binding_hash = 2; */ expectedBindingHash: string; + + /** + * @generated from field: string expected_candidate_hash = 3; + */ + expectedCandidateHash: string; }; /** @@ -1710,7 +1883,7 @@ export type RebindMcpRequest = Message<"engram.app.v1.RebindMcpRequest"> & { * Use `create(RebindMcpRequestSchema)` to create a new message. */ export const RebindMcpRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 58); + messageDesc(file_engram_app_v1_integration, 62); /** * @generated from message engram.app.v1.RebindMcpResponse @@ -1727,7 +1900,7 @@ export type RebindMcpResponse = Message<"engram.app.v1.RebindMcpResponse"> & { * Use `create(RebindMcpResponseSchema)` to create a new message. */ export const RebindMcpResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 59); + messageDesc(file_engram_app_v1_integration, 63); /** * Entered values are request-only; responses contain metadata and references. @@ -1771,7 +1944,7 @@ export type PutMcpCredentialRequest = Message<"engram.app.v1.PutMcpCredentialReq * Use `create(PutMcpCredentialRequestSchema)` to create a new message. */ export const PutMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 60); + messageDesc(file_engram_app_v1_integration, 64); /** * @generated from message engram.app.v1.McpCredentialStatus @@ -1813,7 +1986,7 @@ export type McpCredentialStatus = Message<"engram.app.v1.McpCredentialStatus"> & * Use `create(McpCredentialStatusSchema)` to create a new message. */ export const McpCredentialStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 61); + messageDesc(file_engram_app_v1_integration, 65); /** * @generated from message engram.app.v1.PutMcpCredentialResponse @@ -1835,7 +2008,7 @@ export type PutMcpCredentialResponse = Message<"engram.app.v1.PutMcpCredentialRe * Use `create(PutMcpCredentialResponseSchema)` to create a new message. */ export const PutMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 62); + messageDesc(file_engram_app_v1_integration, 66); /** * @generated from message engram.app.v1.CompleteMcpAuthorizationResponse @@ -1857,7 +2030,7 @@ export type CompleteMcpAuthorizationResponse = Message<"engram.app.v1.CompleteMc * Use `create(CompleteMcpAuthorizationResponseSchema)` to create a new message. */ export const CompleteMcpAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 63); + messageDesc(file_engram_app_v1_integration, 67); /** * @generated from message engram.app.v1.RecoverMcpCredentialResponse @@ -1879,7 +2052,7 @@ export type RecoverMcpCredentialResponse = Message<"engram.app.v1.RecoverMcpCred * Use `create(RecoverMcpCredentialResponseSchema)` to create a new message. */ export const RecoverMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 64); + messageDesc(file_engram_app_v1_integration, 68); /** * @generated from message engram.app.v1.McpCredentialState @@ -1901,7 +2074,7 @@ export type McpCredentialState = Message<"engram.app.v1.McpCredentialState"> & { * Use `create(McpCredentialStateSchema)` to create a new message. */ export const McpCredentialStateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 65); + messageDesc(file_engram_app_v1_integration, 69); /** * @generated from message engram.app.v1.McpApprovedDestination @@ -1923,7 +2096,7 @@ export type McpApprovedDestination = Message<"engram.app.v1.McpApprovedDestinati * Use `create(McpApprovedDestinationSchema)` to create a new message. */ export const McpApprovedDestinationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 66); + messageDesc(file_engram_app_v1_integration, 70); /** * @generated from message engram.app.v1.McpClientRegistration @@ -1970,7 +2143,7 @@ export type McpClientRegistration = Message<"engram.app.v1.McpClientRegistration * Use `create(McpClientRegistrationSchema)` to create a new message. */ export const McpClientRegistrationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 67); + messageDesc(file_engram_app_v1_integration, 71); /** * @generated from message engram.app.v1.GetMcpSetupRequest @@ -1987,7 +2160,7 @@ export type GetMcpSetupRequest = Message<"engram.app.v1.GetMcpSetupRequest"> & { * Use `create(GetMcpSetupRequestSchema)` to create a new message. */ export const GetMcpSetupRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 68); + messageDesc(file_engram_app_v1_integration, 72); /** * @generated from message engram.app.v1.GetMcpSetupResponse @@ -2039,7 +2212,7 @@ export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & * Use `create(GetMcpSetupResponseSchema)` to create a new message. */ export const GetMcpSetupResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 69); + messageDesc(file_engram_app_v1_integration, 73); /** * @generated from message engram.app.v1.CompleteMcpAuthorizationRequest @@ -2091,7 +2264,7 @@ export type CompleteMcpAuthorizationRequest = Message<"engram.app.v1.CompleteMcp * Use `create(CompleteMcpAuthorizationRequestSchema)` to create a new message. */ export const CompleteMcpAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 70); + messageDesc(file_engram_app_v1_integration, 74); /** * @generated from message engram.app.v1.RecoverMcpCredentialRequest @@ -2118,7 +2291,7 @@ export type RecoverMcpCredentialRequest = Message<"engram.app.v1.RecoverMcpCrede * Use `create(RecoverMcpCredentialRequestSchema)` to create a new message. */ export const RecoverMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 71); + messageDesc(file_engram_app_v1_integration, 75); /** * Exact server review. Clients must return it unchanged when they decide. @@ -2232,7 +2405,7 @@ export type McpApprovalReviewContext = Message<"engram.app.v1.McpApprovalReviewC * Use `create(McpApprovalReviewContextSchema)` to create a new message. */ export const McpApprovalReviewContextSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 72); + messageDesc(file_engram_app_v1_integration, 76); /** * @generated from message engram.app.v1.McpApprovalReceipt @@ -2274,7 +2447,7 @@ export type McpApprovalReceipt = Message<"engram.app.v1.McpApprovalReceipt"> & { * Use `create(McpApprovalReceiptSchema)` to create a new message. */ export const McpApprovalReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 73); + messageDesc(file_engram_app_v1_integration, 77); /** * @generated from message engram.app.v1.McpExecutionReview @@ -2364,6 +2537,16 @@ export type McpExecutionReview = Message<"engram.app.v1.McpExecutionReview"> & { * @generated from field: string binding_hash = 17; */ bindingHash: string; + + /** + * @generated from field: engram.app.v1.McpUnknownContext expected_unknown_context = 18; + */ + expectedUnknownContext?: McpUnknownContext; + + /** + * @generated from field: engram.app.v1.McpCancellationReceipt cancellation_receipt = 19; + */ + cancellationReceipt?: McpCancellationReceipt; }; /** @@ -2371,7 +2554,91 @@ export type McpExecutionReview = Message<"engram.app.v1.McpExecutionReview"> & { * Use `create(McpExecutionReviewSchema)` to create a new message. */ export const McpExecutionReviewSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 74); + messageDesc(file_engram_app_v1_integration, 78); + +/** + * @generated from message engram.app.v1.McpUnknownContext + */ +export type McpUnknownContext = Message<"engram.app.v1.McpUnknownContext"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: uint32 journal_revision = 2; + */ + journalRevision: number; + + /** + * @generated from field: string request_digest = 3; + */ + requestDigest: string; + + /** + * @generated from field: string binding_hash = 4; + */ + bindingHash: string; + + /** + * @generated from field: string intent_id = 5; + */ + intentId: string; +}; + +/** + * Describes the message engram.app.v1.McpUnknownContext. + * Use `create(McpUnknownContextSchema)` to create a new message. + */ +export const McpUnknownContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 79); + +/** + * @generated from message engram.app.v1.McpCancellationReceipt + */ +export type McpCancellationReceipt = Message<"engram.app.v1.McpCancellationReceipt"> & { + /** + * @generated from field: string execution_id = 1; + */ + executionId: string; + + /** + * @generated from field: string actor_id = 2; + */ + actorId: string; + + /** + * @generated from field: string cause = 3; + */ + cause: string; + + /** + * @generated from field: uint32 expected_journal_revision = 4; + */ + expectedJournalRevision: number; + + /** + * @generated from field: uint32 journal_revision = 5; + */ + journalRevision: number; + + /** + * @generated from field: string state = 6; + */ + state: string; + + /** + * @generated from field: string created_at = 7; + */ + createdAt: string; +}; + +/** + * Describes the message engram.app.v1.McpCancellationReceipt. + * Use `create(McpCancellationReceiptSchema)` to create a new message. + */ +export const McpCancellationReceiptSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 80); /** * @generated from message engram.app.v1.McpResolutionReceipt @@ -2396,6 +2663,11 @@ export type McpResolutionReceipt = Message<"engram.app.v1.McpResolutionReceipt"> * @generated from field: string created_at = 4; */ createdAt: string; + + /** + * @generated from field: engram.app.v1.McpUnknownContext expected_context = 5; + */ + expectedContext?: McpUnknownContext; }; /** @@ -2403,7 +2675,7 @@ export type McpResolutionReceipt = Message<"engram.app.v1.McpResolutionReceipt"> * Use `create(McpResolutionReceiptSchema)` to create a new message. */ export const McpResolutionReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 75); + messageDesc(file_engram_app_v1_integration, 81); /** * @generated from message engram.app.v1.McpBindingReceipt @@ -2440,7 +2712,7 @@ export type McpBindingReceipt = Message<"engram.app.v1.McpBindingReceipt"> & { * Use `create(McpBindingReceiptSchema)` to create a new message. */ export const McpBindingReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 76); + messageDesc(file_engram_app_v1_integration, 82); /** * @generated from message engram.app.v1.GetSessionMcpStateRequest @@ -2492,7 +2764,7 @@ export type GetSessionMcpStateRequest = Message<"engram.app.v1.GetSessionMcpStat * Use `create(GetSessionMcpStateRequestSchema)` to create a new message. */ export const GetSessionMcpStateRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 77); + messageDesc(file_engram_app_v1_integration, 83); /** * @generated from message engram.app.v1.GetSessionMcpStateResponse @@ -2569,7 +2841,7 @@ export type GetSessionMcpStateResponse = Message<"engram.app.v1.GetSessionMcpSta * Use `create(GetSessionMcpStateResponseSchema)` to create a new message. */ export const GetSessionMcpStateResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 78); + messageDesc(file_engram_app_v1_integration, 84); /** * Immutable session grant. Schemas remain independent documents with their own roots. @@ -2638,7 +2910,38 @@ export type McpBoundToolSummary = Message<"engram.app.v1.McpBoundToolSummary"> & * Use `create(McpBoundToolSummarySchema)` to create a new message. */ export const McpBoundToolSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 79); + messageDesc(file_engram_app_v1_integration, 85); + +/** + * @generated from enum engram.app.v1.McpRebindDrainReason + */ +export enum McpRebindDrainReason { + /** + * @generated from enum value: MCP_REBIND_DRAIN_REASON_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: MCP_REBIND_DRAIN_REASON_DISPATCH_IN_PROGRESS = 1; + */ + DISPATCH_IN_PROGRESS = 1, + + /** + * @generated from enum value: MCP_REBIND_DRAIN_REASON_UNKNOWN_INTENT = 2; + */ + UNKNOWN_INTENT = 2, + + /** + * @generated from enum value: MCP_REBIND_DRAIN_REASON_LEGACY_INTENT = 3; + */ + LEGACY_INTENT = 3, +} + +/** + * Describes the enum engram.app.v1.McpRebindDrainReason. + */ +export const McpRebindDrainReasonSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_engram_app_v1_integration, 0); /** * ADR 0057 C3: orchestrator-native connector catalog CRUD (Plane B). Connectors @@ -2912,6 +3215,14 @@ export const IntegrationService: GenService<{ input: typeof ResolveMcpOutcomeRequestSchema; output: typeof ResolveMcpOutcomeResponseSchema; }, + /** + * @generated from rpc engram.app.v1.IntegrationService.PrepareMcpRebind + */ + prepareMcpRebind: { + methodKind: "unary"; + input: typeof PrepareMcpRebindRequestSchema; + output: typeof PrepareMcpRebindResponseSchema; + }, /** * @generated from rpc engram.app.v1.IntegrationService.RebindMcp */ From 474e214ec7033b1be616b2e36de6134458bdc872 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 06:56:24 -0500 Subject: [PATCH 028/279] Fix MCP rebind review and broker fencing --- .../mcp-rebind-dispatch-race.test.ts | 143 ++++++++++++++++++ orchestrator/src/__tests__/mcp-rebind.test.ts | 117 ++++++++++++++ .../src/__tests__/mcp-review-fixture.ts | 1 + .../src/__tests__/mcp-review-rpc.test.ts | 72 +++++++++ .../__tests__/mcp-runtime-authority.test.ts | 49 +++++- orchestrator/src/mcp/contracts.ts | 7 +- orchestrator/src/mcp/effect-driver.ts | 12 +- orchestrator/src/mcp/rebind.ts | 50 +++++- orchestrator/src/mcp/review-state.ts | 12 +- orchestrator/src/mcp/session-bindings.ts | 10 ++ 10 files changed, 447 insertions(+), 26 deletions(-) diff --git a/orchestrator/src/__tests__/mcp-rebind-dispatch-race.test.ts b/orchestrator/src/__tests__/mcp-rebind-dispatch-race.test.ts index e959036a8..986f51998 100644 --- a/orchestrator/src/__tests__/mcp-rebind-dispatch-race.test.ts +++ b/orchestrator/src/__tests__/mcp-rebind-dispatch-race.test.ts @@ -49,6 +49,7 @@ test("a rebind cannot expose a new binding while the old dispatch is in its fina }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -151,6 +152,7 @@ test("cancel and manual resolution do not waive the original in-flight intent", }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -231,6 +233,7 @@ test("an expired original intent permits rebind and cannot send afterward", asyn }; let offset = 0; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -306,6 +309,7 @@ test("a different-scope barrier commits before the waiting rebind reads its drai }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: crypto.randomUUID(), host, @@ -397,6 +401,7 @@ test("an accepted old call cannot cross the barrier after rebind", async () => { }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -415,3 +420,141 @@ test("an accepted old call cannot cross the barrier after rebind", async () => { await f.cleanup(); } }, 20000); + +test("a held host apply does not block another session broker in the same deployment", async () => { + const { profile } = await import("../db/schema.ts"); + const { canonicalDigest } = await import("../mcp/hash.ts"); + const { eq } = await import("drizzle-orm"); + const first = await effectFixture({ classification: "read" }); + const other = await effectFixture({ classification: "read" }); + let release!: () => void; + const held = new Promise((resolve) => { + release = resolve; + }); + let entered!: () => void; + const ready = new Promise((resolve) => { + entered = resolve; + }); + let commit: Promise | undefined; + let releaseLease!: () => void; + const leaseHeld = new Promise((resolve) => { + releaseLease = resolve; + }); + let leased!: () => void; + const leaseReady = new Promise((resolve) => { + leased = resolve; + }); + let drive: Promise | undefined; + async function waitWithin(promise: Promise) { + let timer: ReturnType | undefined; + try { + await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(Error("test gate timed out")), 3000); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } + } + try { + const [firstProfile] = await first.db + .select() + .from(profile) + .where(eq(profile.id, first.profileId)); + const [otherProfile] = await first.db + .select() + .from(profile) + .where(eq(profile.id, other.profileId)); + const grants = [...firstProfile!.integrationGrants, ...otherProfile!.integrationGrants]; + await first.db + .update(profile) + .set({ + integrationGrants: grants, + mcpGrantPublication: { + revision: 2, + actorId: first.actorId, + grantsHash: canonicalDigest(grants.filter((grant) => grant.mcp)), + }, + }) + .where(eq(profile.id, first.profileId)); + let state = { + sessionId: first.sessionId, + bindingEpoch: first.snapshot.bindingEpoch, + bindingHash: first.snapshot.bindingHash, + policy: first.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return state; + }, + async compareAndApply( + _prev: typeof state, + ack: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + policy: typeof state.policy, + ) { + entered(); + await held; + state = { ...state, bindingHash: ack.bindingHash, policy }; + return ack; + }, + }; + const service = makeMcpRebind({ + db: first.db, + deploymentId: other.deploymentId, + host, + readFrozenNativeManifest: async () => "[]", + readOperatorPolicies: async () => first.operatorPolicies, + readBindingEpoch: async () => first.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }); + const candidate = await service.prepare( + first.sessionId, + first.snapshot.bindingHash, + first.actorId, + ); + const call = await other.driver.acceptToolRequest(other.request); + other.afterLease(async () => { + if (other.leases === 1) { + leased(); + await leaseHeld; + } + }); + drive = other.driver.driveOnce(call.id); + await waitWithin(leaseReady); + commit = service.commit( + first.sessionId, + first.snapshot.bindingHash, + candidate.bindingHash, + first.actorId, + ); + await waitWithin(ready); + releaseLease(); + const deadline = Date.now() + 2000; + let calls = 0; + while (Date.now() < deadline && calls === 0) { + calls = (await other.command("stats")).calls; + if (!calls) await new Promise((resolve) => setTimeout(resolve, 10)); + } + expect(calls).toBe(1); + release(); + await commit; + expect(((await drive) as { state: string }).state).toBe("succeeded"); + } finally { + release(); + releaseLease(); + await commit?.catch(() => {}); + await drive?.catch(() => {}); + await other.cleanup(); + await first.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-rebind.test.ts b/orchestrator/src/__tests__/mcp-rebind.test.ts index 101b8f574..906c0a8f8 100644 --- a/orchestrator/src/__tests__/mcp-rebind.test.ts +++ b/orchestrator/src/__tests__/mcp-rebind.test.ts @@ -39,6 +39,7 @@ test("explicit reviewed rebind installs exact deny acknowledgement before atomic }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -107,6 +108,7 @@ test("typed rebind requires the exact reviewed candidate", async () => { }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -164,6 +166,21 @@ test("typed rebind requires the exact reviewed candidate", async () => { }); expect(candidate.bindingHash).toBeTruthy(); expect(candidate.bindingTools.length).toBeGreaterThan(0); + const { profile } = await import("../db/schema.ts"); + await f.db + .update(profile) + .set({ mcpGrantPublication: null }) + .where(eq(profile.id, f.profileId)); + await expect( + client.prepareMcpRebind({ + sessionId: f.sessionId, + expectedBindingHash: f.snapshot.bindingHash, + }), + ).rejects.toThrow("An administrator must publish MCP grants before rebind"); + await f.db + .update(profile) + .set({ mcpGrantPublication: f.snapshot.profilePublication }) + .where(eq(profile.id, f.profileId)); try { await client.rebindMcp({ sessionId: f.sessionId, @@ -237,6 +254,7 @@ test("lost host acknowledgement leaves old snapshot and preserves installed deni }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -297,6 +315,7 @@ test("stale hashes, lost owner and changed connection cannot apply a reviewed ca }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -368,6 +387,7 @@ test("concurrent commits expose only one reviewed binding", async () => { }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -423,6 +443,7 @@ test("physical placement change after host acknowledgement prevents exposure", a }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -476,6 +497,7 @@ test("changed grant publication rejects commit before host apply", async () => { }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -579,6 +601,7 @@ test("child rebind retains the exact current parent intersection", async () => { }; let parentCurrent = true; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -662,6 +685,7 @@ test("new published leaf grants expand only through the exact reviewed rebind", }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -756,6 +780,7 @@ test("a newly administrator-published leaf grant is adopted only after candidate }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -783,6 +808,7 @@ test("rebind remains closed when the trusted host producer is absent", async () const f = await effectFixture({ classification: "read" }); try { const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, readOperatorPolicies: async () => f.operatorPolicies, @@ -838,6 +864,7 @@ test("managed authority inserted during host acknowledgement prevents exposure", }, }; const service = makeMcpRebind({ + readFrozenNativeManifest: async () => "[]", db: f.db, deploymentId: f.deploymentId, host, @@ -861,3 +888,93 @@ test("managed authority inserted during host acknowledgement prevents exposure", await f.cleanup(); } }, 20000); + +test("rebind rejects missing, oversized and duplicate frozen native manifests before host apply", async () => { + const { appendMcpManifest } = await import("../tools/manifest.ts"); + const f = await effectFixture({ classification: "read" }); + try { + let applied = 0; + const state = { + sessionId: f.sessionId, + bindingEpoch: f.snapshot.bindingEpoch, + bindingHash: f.snapshot.bindingHash, + policy: f.snapshot.managedDenyPolicy, + }; + const host = { + async readApplied() { + return state; + }, + async compareAndApply( + _prev: typeof state, + ack: { + bindingHash: string; + sessionId: string; + bindingEpoch: number; + denyRevision: number; + denyDigest: string; + }, + ) { + applied++; + return ack; + }, + }; + const native = (name: string, description = "x") => ({ + name, + description, + inputSchema: {}, + execution: "sync", + nativeBindings: {}, + }); + const common = { + db: f.db, + deploymentId: f.deploymentId, + host, + readOperatorPolicies: async () => f.operatorPolicies, + readBindingEpoch: async () => f.snapshot.bindingEpoch, + readParentCurrent: async () => { + throw Error("unexpected parent"); + }, + }; + const byteBase = Buffer.byteLength( + JSON.stringify(appendMcpManifest(JSON.stringify([native("large", "")]), f.snapshot)), + ); + const exactBytes = JSON.stringify([native("large", "x".repeat(1024 * 1024 - byteBase))]); + const exactCount = JSON.stringify(Array.from({ length: 127 }, (_, i) => native(`native-${i}`))); + for (const manifest of [exactCount, exactBytes]) { + const service = makeMcpRebind({ ...common, readFrozenNativeManifest: async () => manifest }); + expect( + (await service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId)).bindingHash, + ).toBeTruthy(); + } + const manifests = [ + undefined, + JSON.stringify(Array.from({ length: 128 }, (_, i) => native(`native-${i}`))), + JSON.stringify([native("large", "x".repeat(1024 * 1024 - byteBase + 1))]), + JSON.stringify([native(f.snapshot.immutableBindings[0]!.tools[0]!.name)]), + ]; + let currentManifest: string | undefined = "[]"; + const committing = makeMcpRebind({ + ...common, + readFrozenNativeManifest: async () => currentManifest, + }); + const candidate = await committing.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId); + for (const manifest of manifests) { + const service = makeMcpRebind({ ...common, readFrozenNativeManifest: async () => manifest }); + await expect( + service.prepare(f.sessionId, f.snapshot.bindingHash, f.actorId), + ).rejects.toThrow(); + currentManifest = manifest; + await expect( + committing.commit(f.sessionId, f.snapshot.bindingHash, candidate.bindingHash, f.actorId), + ).rejects.toThrow(); + const [row] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + expect(row!.mcpBindingEnvelope!.bindingHash).toBe(f.snapshot.bindingHash); + } + expect(applied).toBe(0); + } finally { + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-review-fixture.ts b/orchestrator/src/__tests__/mcp-review-fixture.ts index 1c149e9ca..3c08050bd 100644 --- a/orchestrator/src/__tests__/mcp-review-fixture.ts +++ b/orchestrator/src/__tests__/mcp-review-fixture.ts @@ -138,6 +138,7 @@ export async function reviewFixture(documents: Record[] = [], t bound = binding.tools[0]!; const resolvedTool = { bindingHash: snapshot.bindingHash, + profilePublication: snapshot.profilePublication, binding, modelToolName: bound.name, reviewedTool: bound.tool, diff --git a/orchestrator/src/__tests__/mcp-review-rpc.test.ts b/orchestrator/src/__tests__/mcp-review-rpc.test.ts index 1fb314f22..2ab2c808b 100644 --- a/orchestrator/src/__tests__/mcp-review-rpc.test.ts +++ b/orchestrator/src/__tests__/mcp-review-rpc.test.ts @@ -21,6 +21,33 @@ import { registerPassthrough } from "../rpc/passthrough.ts"; const reachable = process.env.ORCHESTRATOR_DATABASE_URL ? await checkDb() : false; async function wire(f: Awaited>, producers = true) { + const { taskSession, profile } = await import("../db/schema.ts"); + const { canonicalDigest } = await import("../mcp/hash.ts"); + const publication = { revision: 1, actorId: f.actorId, grantsHash: f.snapshot.profileGrantsHash }; + const { bindingHash: _oldHash, ...body } = f.snapshot; + f.snapshot.profilePublication = publication; + f.snapshot.bindingHash = canonicalDigest({ ...body, profilePublication: publication }); + f.input.bindingHash = f.snapshot.bindingHash; + f.input.resolvedTool.bindingHash = f.snapshot.bindingHash; + f.input.resolvedTool.profilePublication = publication; + const [sessionRow] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + await f.db + .update(taskSession) + .set({ + mcpBindingEnvelope: { + ...sessionRow!.mcpBindingEnvelope!, + profilePublication: publication, + bindingHash: f.snapshot.bindingHash, + }, + }) + .where(eq(taskSession.sessionId, f.sessionId)); + await f.db + .update(profile) + .set({ mcpGrantPublication: publication }) + .where(eq(profile.id, f.profileId)); const service: McpServices = { enabled: true, repository: f.repository, journal: f.journal }; // Injected policy and resolver are fixtures, not a live coordinator/host ACK. if (producers) @@ -430,3 +457,48 @@ test.skipIf(!reachable)("a fresh policy lineage change invalidates a saved revie await f.cleanup(); } }); + +test.skipIf(!reachable)( + "typed state reports legacy leaf publication as unavailable", + async () => { + const { taskSession } = await import("../db/schema.ts"); + const { canonicalDigest } = await import("../mcp/hash.ts"); + const f = await reviewFixture(); + const s = await wire(f); + try { + const call = await f.journal.create(f.input, { kind: "approval" }, f.now); + await f.journal.requestApproval(call.id, f.deploymentId, f.now); + const published = await s.client.getSessionMcpState({ sessionId: f.sessionId }); + expect(published.availability).toBe("available"); + expect(published.executions[0]!.allowedActions).toContain("approve"); + const { bindingHash: _old, ...body } = f.snapshot; + const legacy = { ...body, profilePublication: null }; + const hash = canonicalDigest(legacy); + f.snapshot.profilePublication = null; + f.snapshot.bindingHash = hash; + const [stored] = await f.db + .select() + .from(taskSession) + .where(eq(taskSession.sessionId, f.sessionId)); + await f.db + .update(taskSession) + .set({ + mcpBindingEnvelope: { + ...stored!.mcpBindingEnvelope!, + profilePublication: null, + bindingHash: hash, + }, + }) + .where(eq(taskSession.sessionId, f.sessionId)); + const state = await s.client.getSessionMcpState({ sessionId: f.sessionId }); + expect(state.availability).toBe("unavailable"); + expect(state.bindingReceipt?.staleReasons).toContain("publication_required"); + expect(state.executions[0]!.allowedActions).not.toContain("approve"); + expect(state.writePolicy?.allowedActions ?? []).not.toContain("approve_current_plan"); + } finally { + await s.close(); + await f.cleanup(); + } + }, + 20000, +); diff --git a/orchestrator/src/__tests__/mcp-runtime-authority.test.ts b/orchestrator/src/__tests__/mcp-runtime-authority.test.ts index b0c56ba4d..fade3ff43 100644 --- a/orchestrator/src/__tests__/mcp-runtime-authority.test.ts +++ b/orchestrator/src/__tests__/mcp-runtime-authority.test.ts @@ -221,18 +221,53 @@ test("competing barriers commit once and losing cleanup preserves the committed } }); -test("barrier and human cancellation share a safe lock order", async () => { +test("cancellation commits before a waiting dispatch", async () => { const f = await ready(); try { - const results = await Promise.allSettled([ - f.journal.dispatch(f.call.id, f.intent, f.now, f.proof), - f.journal.cancel(f.call.id, f.actorId, f.now, (await f.journal.get(f.call.id))!.version), - ]); - expect(results[1]!.status).toBe("fulfilled"); - expect(["canceled", "outcome_unknown"]).toContain((await f.journal.get(f.call.id))!.state); + const revision = (await f.journal.get(f.call.id))!.version; + let release!: () => void; + const gate = new Promise((resolve) => { + release = resolve; + }); + const dispatch = (async () => { + await gate; + return f.journal.dispatch(f.call.id, f.intent, f.now, f.proof); + })(); + const canceled = await f.journal.cancel(f.call.id, f.actorId, f.now, revision); + expect(canceled.state).toBe("canceled"); + release(); + await expect(dispatch).rejects.toThrow(); + expect((await f.journal.get(f.call.id))!.state).toBe("canceled"); expect(await f.journal.getAdmission(f.call.id)).toBeNull(); await expect(f.journal.assertDispatchCurrent(f.call.id, f.proof, f.now)).rejects.toThrow(); } finally { await f.cleanup(); } }); + +test("committed dispatch rejects cancellation with the previously observed revision", async () => { + const f = await ready(); + try { + const revision = (await f.journal.get(f.call.id))!.version; + let release!: () => void; + const gate = new Promise((resolve) => { + release = resolve; + }); + const cancel = (async () => { + await gate; + return f.journal.cancel(f.call.id, f.actorId, f.now, revision); + })(); + expect((await f.journal.dispatch(f.call.id, f.intent, f.now, f.proof)).state).toBe( + "dispatching", + ); + release(); + await expect(cancel).rejects.toThrow("stale cancellation context"); + const call = (await f.journal.get(f.call.id))!; + expect(call.state).toBe("dispatching"); + expect(call.intentId).toBe(f.intent.intentId); + expect((await f.journal.getAdmission(f.call.id))!.reservationId).toBe(f.proof.reservationId); + await expect(f.journal.dispatch(f.call.id, f.intent, f.now, f.proof)).rejects.toThrow(); + } finally { + await f.cleanup(); + } +}); diff --git a/orchestrator/src/mcp/contracts.ts b/orchestrator/src/mcp/contracts.ts index 4a41eb5da..32080ea08 100644 --- a/orchestrator/src/mcp/contracts.ts +++ b/orchestrator/src/mcp/contracts.ts @@ -5,7 +5,7 @@ import { McpRebindDrainSchema, McpRebindDrainReason, } from "../gen/engram/app/v1/integration_pb.ts"; -import { McpRebindDraining, type McpRebindDrain } from "./rebind.ts"; +import { McpPublicationRequired, McpRebindDraining, type McpRebindDrain } from "./rebind.ts"; import { policyReviewCurrent, type JournalReviewContext } from "./journal-review.ts"; import { credentialHandlers } from "./credential-rpc.ts"; import type { CredentialAdmin } from "./credential-admin.ts"; @@ -75,6 +75,11 @@ export async function mcpOperation(run: () => Promise): Promise { return await run(); } catch (error) { if (error instanceof ConnectError) throw error; + if (error instanceof McpPublicationRequired) + throw new ConnectError( + "An administrator must publish MCP grants before rebind", + Code.FailedPrecondition, + ); if (error instanceof McpRebindDraining) throw new ConnectError("MCP calls are still draining", Code.FailedPrecondition, undefined, [ { desc: McpRebindDrainSchema, value: drainProto(error.drain) }, diff --git a/orchestrator/src/mcp/effect-driver.ts b/orchestrator/src/mcp/effect-driver.ts index f388dc921..c5ae35b24 100644 --- a/orchestrator/src/mcp/effect-driver.ts +++ b/orchestrator/src/mcp/effect-driver.ts @@ -28,7 +28,7 @@ import { type SessionBindingResolver, type ResolvedBoundTool, } from "./session-resolver.ts"; -import type { McpSessionBindingSnapshot } from "./session-bindings.ts"; +import { hasPublishedLeaf, type McpSessionBindingSnapshot } from "./session-bindings.ts"; import { makeResourceWorkerClient } from "./worker-client.ts"; import { toWorkerBindingHash } from "./worker-encoding.ts"; @@ -86,15 +86,7 @@ function argumentsValue(raw: string): Record { } } function publication(snapshot: McpSessionBindingSnapshot) { - const value = snapshot.profilePublication; - if ( - !value || - !Number.isSafeInteger(value.revision) || - value.revision < 1 || - !value.actorId || - value.grantsHash !== snapshot.profileGrantsHash - ) - throw new McpRequestDenied("publication_required"); + if (!hasPublishedLeaf(snapshot)) throw new McpRequestDenied("publication_required"); } /** The authenticated worker envelope is verified by the client and broker first. */ diff --git a/orchestrator/src/mcp/rebind.ts b/orchestrator/src/mcp/rebind.ts index 6930473e6..6852b221c 100644 --- a/orchestrator/src/mcp/rebind.ts +++ b/orchestrator/src/mcp/rebind.ts @@ -7,7 +7,7 @@ import { taskSession, type IntegrationConnectionSnapshot, } from "../db/schema.ts"; -import { lockAdmissionScope } from "./admission.ts"; +import { appendMcpManifest } from "../tools/manifest.ts"; import { bindingSnapshotColumns, readLiveBindingAuthority, @@ -25,6 +25,11 @@ import { } from "./session-bindings.ts"; import { expectedManagedDenyAck } from "./session-resolver.ts"; +export class McpPublicationRequired extends Error { + constructor() { + super("MCP publication required"); + } +} export type McpRebindDrain = { reason: "dispatch_in_progress" | "unknown_intent" | "legacy_intent"; retryAfter: string; @@ -54,6 +59,9 @@ export function makeMcpRebind(deps: { db?: ReturnType; deploymentId: string; host?: McpRebindHost; + /** Trusted producer returns the exact native manifest frozen at creation for this session. + * It must reject missing or unverifiable storage; guest input and profile rebuilds are not authority. */ + readFrozenNativeManifest: (sessionId: string) => Promise; readOperatorPolicies(): Promise; readBindingEpoch(id: string): Promise; readParentCurrent(id: string): Promise; @@ -65,7 +73,6 @@ export function makeMcpRebind(deps: { throw Error("MCP rebind service unavailable"); const host = deps.host; return db.transaction(async (tx) => { - await lockAdmissionScope(tx, deps.deploymentId); const { session, owner, ancestors } = await lockHumanSession( tx, sessionId, @@ -98,18 +105,45 @@ export function makeMcpRebind(deps: { ...ancestors.map((row) => row.session.profileId).filter((id): id is string => !!id), ]), ].sort(); - await tx + const profiles = await tx .select() .from(profile) .where(inArray(profile.id, profileIds)) .orderBy(asc(profile.id)) .for("share"); + const connectionIds = [ + ...new Set([ + ...old.immutableBindings.map((binding) => binding.connectionId), + ...profiles.flatMap((row) => + row.integrationGrants.filter((grant) => grant.mcp).map((grant) => grant.connectionId), + ), + ...ancestors.flatMap(({ session: parent }) => + parent.mcpBindingEnvelope + ? restoreBindingSnapshot( + parent.mcpBindingEnvelope, + parent.integrationConnections ?? [], + ).immutableBindings.map((binding) => binding.connectionId) + : [], + ), + ]), + ].sort(); + if (connectionIds.length) + await tx + .select({ id: integrationConnection.id }) + .from(integrationConnection) + .where( + and( + eq(integrationConnection.provider, "mcp"), + inArray(integrationConnection.id, connectionIds), + ), + ) + .orderBy(asc(integrationConnection.id)) + .for("share"); const connections = await tx .select() .from(integrationConnection) .where(eq(integrationConnection.provider, "mcp")) - .orderBy(asc(integrationConnection.id)) - .for("share"); + .orderBy(asc(integrationConnection.id)); // A broker may already be beyond its last database check. The original intent // expiry bounds credential delivery even after cancellation or manual resolution. const [pending] = await tx @@ -159,7 +193,7 @@ export function makeMcpRebind(deps: { live.profilePublication.grantsHash !== canonicalDigest(live.profileGrants.filter((grant) => grant.mcp !== undefined)) ) - throw Error("MCP grant publication unavailable"); + throw new McpPublicationRequired(); const epoch = await bounded(() => deps.readBindingEpoch(sessionId)); const applied = await bounded(() => host.readApplied(sessionId, epoch)); let parentAuthority: { sessionId: string; snapshot: McpSessionBindingSnapshot } | undefined; @@ -204,6 +238,10 @@ export function makeMcpRebind(deps: { const { bindingHash: _hash, ...body } = { ...compiled, managedDenyPolicy: deny }; const candidate = { ...body, bindingHash: canonicalDigest(body) }; compileMcpManifest(candidate); + if (!deps.readFrozenNativeManifest) throw Error("MCP native manifest unavailable"); + const nativeManifest = await bounded(() => deps.readFrozenNativeManifest(sessionId)); + if (nativeManifest === undefined) throw Error("MCP native manifest unavailable"); + appendMcpManifest(nativeManifest, candidate); if (candidateHash !== undefined) { if (candidate.bindingHash !== candidateHash) throw Error("stale MCP rebind candidate"); await bounded(() => diff --git a/orchestrator/src/mcp/review-state.ts b/orchestrator/src/mcp/review-state.ts index 087c283de..0cac0a3a3 100644 --- a/orchestrator/src/mcp/review-state.ts +++ b/orchestrator/src/mcp/review-state.ts @@ -1,3 +1,4 @@ +import { hasPublishedLeaf } from "./session-bindings.ts"; import { create, toBinary, toJson, type MessageInitShape } from "@bufbuild/protobuf"; import { GetSessionMcpStateResponseSchema } from "../gen/engram/app/v1/integration_pb.ts"; import { unknownContext, type McpJournal } from "./journal.ts"; @@ -6,6 +7,8 @@ import { restoreBindingSnapshot } from "./binding-store.ts"; import type { SessionBindingResolver } from "./session-resolver.ts"; import type { SessionPolicyControl } from "./session-policy.ts"; +class PublicationRequired extends Error {} + function stateSize(value: MessageInitShape): number { const message = create(GetSessionMcpStateResponseSchema, value); return Math.max( @@ -106,6 +109,10 @@ export async function sessionMcpState(input: { }; try { snapshot = restoreBindingSnapshot(envelope, data.session.integrationConnections ?? []); + if (!hasPublishedLeaf(snapshot)) { + response.bindingReceipt.staleReasons = ["publication_required"]; + throw new PublicationRequired(); + } if (!input.producers?.policy.readWritePolicy) throw new Error("live producers missing"); const live = await input.producers.bindings.readCurrent(input.sessionId); const policy = await input.producers.policy.readWritePolicy({ @@ -125,8 +132,9 @@ export async function sessionMcpState(input: { response.bindingReceipt.current = true; response.bindingReceipt.staleReasons = []; response.availability = "available"; - } catch { - response.bindingReceipt.staleReasons = ["binding_or_policy_unavailable"]; + } catch (error) { + if (!(error instanceof PublicationRequired)) + response.bindingReceipt.staleReasons = ["binding_or_policy_unavailable"]; } } if (input.canRebind && snapshot && response.bindingReceipt) diff --git a/orchestrator/src/mcp/session-bindings.ts b/orchestrator/src/mcp/session-bindings.ts index 86ec1e832..df1005d8d 100644 --- a/orchestrator/src/mcp/session-bindings.ts +++ b/orchestrator/src/mcp/session-bindings.ts @@ -125,6 +125,16 @@ export function managedAuthorityHash(live: McpLiveAuthority) { .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), ); } +export function hasPublishedLeaf(snapshot: McpSessionBindingSnapshot): boolean { + const value = snapshot.profilePublication; + return ( + !!value && + Number.isSafeInteger(value.revision) && + value.revision >= 1 && + !!value.actorId && + value.grantsHash === snapshot.profileGrantsHash + ); +} function effectiveGrants(snapshot: McpSessionBindingSnapshot): ProfileIntegrationGrant[] { return snapshot.immutableBindings.flatMap((binding) => binding.tools.map((tool) => ({ From 96f4f01967c8aecaa1cbc0eff58ad8cd609c8f01 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 08:42:34 -0500 Subject: [PATCH 029/279] Add authenticated durable managed egress policy control Retain monotone exact-host denies before host I/O, apply them through a pinned mutual-TLS host service, and return durable placement-bound CAS receipts. Drain stable session socket leases before proxy policy changes. Keep managed boot and final enforcement closed without backend network proof. Add lifetime directory ownership, startup proxy quarantine, strict HTTP authority checks, and opaque-tunnel rejection. Version the shared wire and generated policy service contracts. Validate local TLS, durable crash recovery, socket drain, paired Sim/PG semantics, generated bytes, and native/Linux musl scoped Clippy. Actual backend quarantine, boot finalization, operator publication, and platform acceptance remain separate required producers. --- Cargo.lock | 5 + crates/engram-coordinator/src/api/sessions.rs | 15 +- crates/engram-coordinator/src/api/snapshot.rs | 1 + .../engram-coordinator/src/host_registry.rs | 18 + crates/engram-coordinator/src/main.rs | 8 +- crates/engram-coordinator/src/mcp/policy.rs | 132 +++ crates/engram-coordinator/src/session_boot.rs | 16 + crates/engram-core/src/traits/host_client.rs | 16 + crates/engram-core/src/traits/metadata.rs | 19 + crates/engram-core/src/traits/sandbox.rs | 13 + crates/engram-core/src/types/egress.rs | 13 +- crates/engram-core/src/types/integration.rs | 23 + crates/engram-core/src/types/managed_deny.rs | 296 ++++++ crates/engram-core/src/types/mod.rs | 2 + crates/engram-dst/tests/exactly_once.rs | 1 + crates/engram-egress-proto/src/adopt.rs | 1 + crates/engram-egress-proto/src/lib.rs | 10 +- crates/engram-egress-proxy/src/connections.rs | 86 ++ crates/engram-egress-proxy/src/dns.rs | 11 + .../engram-egress-proxy/src/guest_gateway.rs | 115 ++- crates/engram-egress-proxy/src/intercept.rs | 186 +++- crates/engram-egress-proxy/src/lib.rs | 2 + crates/engram-egress-proxy/src/proxy.rs | 44 +- crates/engram-egress-proxy/src/registry.rs | 246 ++++- .../tests/intercept_e2e.rs | 121 +++ crates/engram-egress-proxyd/src/control.rs | 143 ++- crates/engram-egress-proxyd/src/lib.rs | 1 + .../engram-egress-proxyd/tests/spawn_serve.rs | 1 + crates/engram-host-agent/src/bindings.rs | 33 + crates/engram-host-agent/src/config.rs | 2 + crates/engram-host-agent/src/grpc_server.rs | 127 ++- crates/engram-host-agent/src/harness.rs | 4 + crates/engram-host-agent/src/host_client.rs | 83 +- crates/engram-host-agent/src/lib.rs | 82 +- crates/engram-host-agent/src/main.rs | 8 + .../engram-host-agent/src/managed_policy.rs | 881 ++++++++++++++++++ crates/engram-host-agent/src/managed_tls.rs | 35 + .../engram-host-agent/src/pooled_backend.rs | 13 +- crates/engram-host-agent/src/proxyd_client.rs | 51 +- crates/engram-host-agent/tests/e2e_harness.rs | 2 + .../tests/egress_stream_survives_roll.rs | 1 + .../tests/egress_survives_roll.rs | 1 + crates/engram-host-agent/tests/managed_tls.rs | 450 +++++++++ crates/engram-postgres/src/lib.rs | 73 +- crates/engram-protocol/Cargo.toml | 5 + .../proto/engram/mcp/v1/session_policy.proto | 60 ++ .../engram-protocol/proto/host_service.proto | 8 + crates/engram-protocol/src/grpc_client.rs | 32 + crates/engram-protocol/src/grpc_pool.rs | 24 +- crates/engram-protocol/src/host_tls.rs | 141 +++ crates/engram-protocol/src/lib.rs | 4 + crates/engram-protocol/src/managed_policy.rs | 178 ++++ crates/engram-protocol/src/wire.rs | 2 +- .../tests/decode_never_panics.rs | 6 + .../tests/golden/session_egress_policy.bin | Bin 265 -> 266 bytes .../golden/session_egress_policy_google.bin | Bin 693 -> 694 bytes .../golden/session_egress_policy_managed.bin | Bin 0 -> 405 bytes .../golden/session_egress_policy_minted.bin | Bin 833 -> 834 bytes .../golden/session_egress_policy_oauth.bin | Bin 498 -> 499 bytes .../session_egress_policy_oauth_user.bin | Bin 514 -> 515 bytes crates/engram-protocol/tests/support/mod.rs | 7 + crates/engram-protocol/tests/wire_golden.rs | 25 +- crates/engram-sandbox-process/src/lib.rs | 1 + crates/engram-sim/src/meta/store_impl.rs | 63 +- crates/engram-sim/tests/meta_conformance.rs | 58 ++ docs/adr/0125-isolated-external-mcp.md | 66 ++ .../gen/engram/mcp/v1/session_policy_pb.ts | 318 ++++++- ...olicy-SessionPolicyService_connectquery.ts | 10 + .../gen/engram/mcp/v1/session_policy_pb.ts | 318 ++++++- 69 files changed, 4572 insertions(+), 145 deletions(-) create mode 100644 crates/engram-core/src/types/managed_deny.rs create mode 100644 crates/engram-egress-proxy/src/connections.rs create mode 100644 crates/engram-host-agent/src/managed_policy.rs create mode 100644 crates/engram-host-agent/src/managed_tls.rs create mode 100644 crates/engram-host-agent/tests/managed_tls.rs create mode 100644 crates/engram-protocol/src/host_tls.rs create mode 100644 crates/engram-protocol/src/managed_policy.rs create mode 100644 crates/engram-protocol/tests/golden/session_egress_policy_managed.bin diff --git a/Cargo.lock b/Cargo.lock index 6b18db503..b50908cb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2906,11 +2906,16 @@ dependencies = [ "dashmap", "engram-core", "engram-telemetry", + "engram-tls", "futures", + "hex", "proptest", "prost", + "rustls", + "rustls-pemfile", "serde", "serde_json", + "sha2 0.11.1", "tokio", "tokio-stream", "tonic", diff --git a/crates/engram-coordinator/src/api/sessions.rs b/crates/engram-coordinator/src/api/sessions.rs index 1bab92514..c68362f5a 100644 --- a/crates/engram-coordinator/src/api/sessions.rs +++ b/crates/engram-coordinator/src/api/sessions.rs @@ -319,13 +319,19 @@ pub(crate) async fn build_resume_egress_policy( sandbox_id: engram_core::SandboxId, image: &str, ) -> Result, ApiError> { + let policy = load_session_policy(state, session_id).await?; let Some(guest_ip) = state.services.host.guest_ip(sandbox_id).await else { + if policy.as_ref().is_some_and(|p| p.managed_deny.is_some()) { + return Err(ApiError::BadRequest( + "managed policy requires an enforceable guest identity".into(), + )); + } return Ok(None); }; // ADR 0057: re-read the persisted session policy once → network + secrets + // injects (resolved host-side) + observes (pure), so a resumed session // re-derives its whole egress policy on the new host (same as create). - let policy = load_session_policy(state, session_id).await?; + let (repo, tag) = split_image_ref(image); let secret_ctx = SecretContext { repo, @@ -352,7 +358,7 @@ pub(crate) async fn build_resume_egress_policy( .await? .map(|rs| rs.apps) .unwrap_or_default(); - Ok(Some(assemble_resume_egress_policy( + let mut result = assemble_resume_egress_policy( session_id, sandbox_id, guest_ip, @@ -369,7 +375,9 @@ pub(crate) async fn build_resume_egress_policy( .map(|policy| policy.tunnels.clone()) .unwrap_or_default(), apps, - ))) + ); + result.managed_deny = policy.and_then(|p| p.managed_deny); + Ok(Some(result)) } /// Pure synchronous assembly path for the resume egress policy. @@ -392,6 +400,7 @@ pub(crate) fn assemble_resume_egress_policy( apps: Vec, ) -> engram_core::types::egress::SessionEgressPolicy { engram_core::types::egress::SessionEgressPolicy { + managed_deny: None, session_id, sandbox_id, guest_ip, diff --git a/crates/engram-coordinator/src/api/snapshot.rs b/crates/engram-coordinator/src/api/snapshot.rs index f2c0f6b21..6185b7a56 100644 --- a/crates/engram-coordinator/src/api/snapshot.rs +++ b/crates/engram-coordinator/src/api/snapshot.rs @@ -56,6 +56,7 @@ pub(crate) fn placeholder_egress_policy( sandbox_id: SandboxId, ) -> engram_core::types::egress::SessionEgressPolicy { engram_core::types::egress::SessionEgressPolicy { + managed_deny: None, session_id, sandbox_id, guest_ip: std::net::Ipv4Addr::UNSPECIFIED, diff --git a/crates/engram-coordinator/src/host_registry.rs b/crates/engram-coordinator/src/host_registry.rs index 374ab09b8..b6814e6e1 100644 --- a/crates/engram-coordinator/src/host_registry.rs +++ b/crates/engram-coordinator/src/host_registry.rs @@ -706,6 +706,24 @@ impl HostClient for HostRegistry { result } + async fn read_managed_policy( + &self, + target: engram_core::types::managed_deny::ManagedPolicyTarget, + ) -> Result, SandboxError> { + self.backend_for(target.host_id) + .await? + .read_managed_policy(target) + .await + } + async fn apply_managed_policy( + &self, + request: engram_core::types::managed_deny::ManagedPolicyApply, + ) -> Result { + self.backend_for(request.context.target.host_id) + .await? + .apply_managed_policy(request) + .await + } async fn start_agent( &self, id: SandboxId, diff --git a/crates/engram-coordinator/src/main.rs b/crates/engram-coordinator/src/main.rs index 172563e96..1634f8c3b 100644 --- a/crates/engram-coordinator/src/main.rs +++ b/crates/engram-coordinator/src/main.rs @@ -762,7 +762,13 @@ async fn main() -> Result<(), CoordinatorError> { // ADR 0013: per-pod gRPC pool. Empty at construction; populated // on `/api/hosts/register` POSTs and (in `run_with_registry`) at // startup from already-registered `hosts` rows. - let host_pool = Arc::new(engram_protocol::grpc_pool::GrpcHostPool::new()); + let mut host_pool = engram_protocol::grpc_pool::GrpcHostPool::new(); + if let Some(tls) = + engram_protocol::host_tls::HostClientTls::from_env().map_err(CoordinatorError::Config)? + { + host_pool = host_pool.with_tls(tls); + } + let host_pool = Arc::new(host_pool); // ADR 0047: let the registry dial hosts this replica has never // fielded a heartbeat from (multi-replica routing read-through). host_registry.set_dialer(host_pool.clone()); diff --git a/crates/engram-coordinator/src/mcp/policy.rs b/crates/engram-coordinator/src/mcp/policy.rs index c2bdb7572..560a19135 100644 --- a/crates/engram-coordinator/src/mcp/policy.rs +++ b/crates/engram-coordinator/src/mcp/policy.rs @@ -107,6 +107,120 @@ fn creation(request: &wire::CreatePolicySessionRequest) -> Result, + ) -> Result, Status> { + let _admission = self.control.check(&req, req.get_ref().protocol)?; + self.control + .authority + .enabled() + .map_err(|_| Status::unavailable("policy unavailable"))?; + let target: engram_core::types::managed_deny::ManagedPolicyTarget = + required(req.into_inner().target)? + .try_into() + .map_err(Status::invalid_argument)?; + let state = self + .state + .as_ref() + .ok_or_else(|| Status::unavailable("managed policy unavailable"))?; + check_managed_target(state, &target).await?; + let applied = state + .services + .host + .read_managed_policy(target.clone()) + .await + .map_err(|_| Status::failed_precondition("host policy unavailable"))?; + check_managed_target(state, &target).await?; + if let Some(receipt) = &applied { + receipt.validate().map_err(Status::failed_precondition)?; + if receipt.context.target != target { + return Err(Status::failed_precondition("host policy identity changed")); + } + } + Ok(Response::new(wire::ReadManagedPolicyResponse { + protocol: wire::PROTOCOL, + applied: applied.map(Into::into), + })) + } + async fn apply_managed_policy( + &self, + req: Request, + ) -> Result, Status> { + let _admission = self.control.check(&req, req.get_ref().protocol)?; + self.control + .authority + .enabled() + .map_err(|_| Status::unavailable("policy unavailable"))?; + let r = req.into_inner(); + let context: engram_core::types::managed_deny::ManagedPolicyContext = required(r.context)? + .try_into() + .map_err(Status::invalid_argument)?; + let policy: engram_core::types::managed_deny::ManagedDenyPolicy = required(r.policy)? + .try_into() + .map_err(Status::invalid_argument)?; + let expected = r + .expected + .map(TryInto::try_into) + .transpose() + .map_err(Status::invalid_argument)?; + let state = self + .state + .as_ref() + .ok_or_else(|| Status::unavailable("managed policy unavailable"))?; + let target = &context.target; + check_managed_target(state, target).await?; + // The desired union commits before any host call. A lost ACK cannot discard it on migration. + state + .services + .meta + .retain_managed_deny(target, &policy) + .await + .map_err(err)?; + let session = state + .services + .meta + .get_session(target.session_id) + .await + .map_err(err)?; + let egress = crate::api::sessions::build_resume_egress_policy( + state, + target.session_id, + target.sandbox_id, + &session.image.to_string(), + ) + .await + .map_err(|_| Status::failed_precondition("managed egress source unavailable"))? + .ok_or_else(|| Status::failed_precondition("managed guest identity unavailable"))?; + if !egress.tunnels.is_empty() { + return Err(Status::failed_precondition( + "managed policy does not support tunnels", + )); + } + if egress.managed_deny.as_ref() != Some(&policy) { + return Err(Status::failed_precondition("desired policy changed")); + } + check_managed_target(state, target).await?; + let applied = state + .services + .host + .apply_managed_policy(engram_core::types::managed_deny::ManagedPolicyApply { + expected, + context: context.clone(), + egress, + }) + .await + .map_err(|_| Status::failed_precondition("host policy unavailable"))?; + check_managed_target(state, target).await?; + applied.validate().map_err(Status::failed_precondition)?; + if applied.context != context || applied.policy != policy { + return Err(Status::failed_precondition("host acknowledgement mismatch")); + } + Ok(Response::new(wire::ApplyManagedPolicyResponse { + protocol: wire::PROTOCOL, + applied: Some(applied.into()), + })) + } async fn create_policy_session( &self, req: Request, @@ -307,6 +421,24 @@ impl SessionPolicyService for PolicyControl { })) } } + +async fn check_managed_target( + state: &SharedState, + target: &engram_core::types::managed_deny::ManagedPolicyTarget, +) -> Result<(), Status> { + if state + .services + .meta + .managed_policy_target(target.session_id) + .await + .map_err(err)? + != *target + { + return Err(Status::failed_precondition("physical placement changed")); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/engram-coordinator/src/session_boot.rs b/crates/engram-coordinator/src/session_boot.rs index 60af8191f..2a1a8f6f2 100644 --- a/crates/engram-coordinator/src/session_boot.rs +++ b/crates/engram-coordinator/src/session_boot.rs @@ -363,6 +363,9 @@ pub(crate) async fn boot_on_reserved_host( let egress_secret_count = egress_secrets.len(); let inject_count = injects.len(); let resolved_policy = ResolvedEgressPolicy { + managed_deny: integration_policy + .as_ref() + .and_then(|p| p.managed_deny.clone()), secrets: egress_secrets, injects, observes, @@ -493,6 +496,15 @@ pub(crate) async fn boot_on_reserved_host( // the next thing that trips this is as invisible as the VZ boot race // was (a 2s guest-IP timeout that lost to a 2.5s guest boot; fixed in // `engram-sandbox-vz`'s `guest_endpoints`, which now waits). + if egress_policy.is_none() + && integration_policy + .as_ref() + .is_some_and(|p| p.managed_deny.is_some()) + { + return Err(BootError::NotStarted(ApiError::BadRequest( + "managed policy requires an enforceable guest identity".into(), + ))); + } let policy = egress_policy.unwrap_or_else(|| { tracing::warn!( %session_id, @@ -505,6 +517,7 @@ pub(crate) async fn boot_on_reserved_host( guest traffic as UnknownGuest and brokered credentials will not substitute", ); engram_core::types::egress::SessionEgressPolicy { + managed_deny: None, session_id, sandbox_id, guest_ip: std::net::Ipv4Addr::UNSPECIFIED, @@ -674,6 +687,7 @@ fn map_restore_error(e: engram_core::SandboxError) -> ApiError { /// IP (process backend / some VZ configs) — the caller synthesizes an /// unspecified-IP fallback. struct ResolvedEgressPolicy { + managed_deny: Option, secrets: Vec, injects: Vec, observes: Vec, @@ -692,6 +706,7 @@ async fn assemble_egress_policy( ) -> Option { let guest_ip = state.services.host.guest_ip(sandbox_id).await?; Some(engram_core::types::egress::SessionEgressPolicy { + managed_deny: resolved.managed_deny, session_id, sandbox_id, guest_ip, @@ -753,6 +768,7 @@ pub(crate) fn assemble_capture_egress_policy( return None; } Some(engram_core::types::egress::SessionEgressPolicy { + managed_deny: None, session_id, // Stamped by the host at registration (the capture VM doesn't // exist yet when this is assembled). diff --git a/crates/engram-core/src/traits/host_client.rs b/crates/engram-core/src/traits/host_client.rs index 8ed874d6d..e2163b63a 100644 --- a/crates/engram-core/src/traits/host_client.rs +++ b/crates/engram-core/src/traits/host_client.rs @@ -381,6 +381,22 @@ pub trait HostClient: Send + Sync { /// eliminates the WS-era frame-ordering invariant that the /// stateless transport can't honour. Local impl applies in-proc; /// the gRPC impl ships both fields in a single `StartAgent` RPC. + async fn read_managed_policy( + &self, + _target: crate::types::managed_deny::ManagedPolicyTarget, + ) -> Result, SandboxError> { + Err(SandboxError::InvalidSpec( + "managed host mode unavailable".into(), + )) + } + async fn apply_managed_policy( + &self, + _request: crate::types::managed_deny::ManagedPolicyApply, + ) -> Result { + Err(SandboxError::InvalidSpec( + "managed host mode unavailable".into(), + )) + } async fn start_agent( &self, id: SandboxId, diff --git a/crates/engram-core/src/traits/metadata.rs b/crates/engram-core/src/traits/metadata.rs index 40b30fb6a..b299e76c6 100644 --- a/crates/engram-core/src/traits/metadata.rs +++ b/crates/engram-core/src/traits/metadata.rs @@ -3414,6 +3414,25 @@ pub trait MetadataStore: Send + Sync { // injects without the orchestrator. One blob per session (upsert). Default // impls (no-op / None) so test doubles compile; `PostgresStore` is the // authority. + /// Exact current physical placement. Missing and unbound sessions fail closed. + async fn managed_policy_target( + &self, + _session_id: SessionId, + ) -> Result { + Err(MetaError::Conflict( + "managed policy producer unavailable".into(), + )) + } + /// Save the monotone desired union before host I/O, without changing placement. + async fn retain_managed_deny( + &self, + _target: &crate::types::managed_deny::ManagedPolicyTarget, + _policy: &crate::types::managed_deny::ManagedDenyPolicy, + ) -> Result<(), MetaError> { + Err(MetaError::Conflict( + "managed policy producer unavailable".into(), + )) + } async fn bind_session_integration_policy( &self, session_id: SessionId, diff --git a/crates/engram-core/src/traits/sandbox.rs b/crates/engram-core/src/traits/sandbox.rs index 38bab4850..4d081aedc 100644 --- a/crates/engram-core/src/traits/sandbox.rs +++ b/crates/engram-core/src/traits/sandbox.rs @@ -167,6 +167,14 @@ pub enum AgentRefresh { #[async_trait] pub trait SandboxBackend: Send + Sync { + /// Strict host-owned network enforcement for this exact placement. Unavailable + /// until the backend can prove all egress paths are controlled before execution. + async fn managed_network_proof( + &self, + _target: &crate::types::managed_deny::ManagedPolicyTarget, + ) -> Option { + None + } async fn create(&self, spec: SandboxSpec) -> Result; /// Start the long-running agent process for this sandbox (the @@ -240,6 +248,11 @@ pub trait SandboxBackend: Send + Sync { /// authenticate or intercept the guest caller. Other policies remain a /// no-op for Process and in-test fixtures. ADR 0006 and ADR 0109. async fn notify_session_policy(&self, policy: SessionEgressPolicy) -> Result<(), SandboxError> { + if policy.managed_deny.is_some() { + return Err(SandboxError::InvalidSpec( + "managed policy requires a host proxy".into(), + )); + } if !policy.guest_services.is_empty() || !policy.tunnels.is_empty() { return Err(SandboxError::InvalidSpec( "guest services and tunnels require a session-scoped guest gateway".to_string(), diff --git a/crates/engram-core/src/types/egress.rs b/crates/engram-core/src/types/egress.rs index 026ff01d5..dd3b97404 100644 --- a/crates/engram-core/src/types/egress.rs +++ b/crates/engram-core/src/types/egress.rs @@ -71,18 +71,19 @@ pub struct SessionEgressPolicy { /// straight back into the sibling's guest port instead of sending it out to /// the internet. /// - /// LAST on purpose. This type crosses the coord↔host wire as positional + /// This type crosses the coord↔host wire as positional /// bincode (ADR 0013), so field ORDER is the format: an inserted /// non-trailing field shifts every byte after it and desyncs a peer built - /// from an older tree across a roll. A trailing field is the only wire-safe - /// evolution — see `engram-protocol/tests/wire_golden.rs`, which pins these + /// from an older tree across a roll. Append fields and bump the wire version + /// on evolution — see `engram-protocol/tests/wire_golden.rs`, which pins these /// bytes and is what catches the mistake. /// - /// `#[serde(default)]` so policies serialized before this field decode with - /// none, and the short circuit simply never fires — the pre-ADR-0118 - /// behaviour. + /// The default applies to JSON only; positional bincode requires matching versions. #[serde(default)] pub apps: Vec, + /// Mandatory exact-host deny. Appended: bincode field order is the wire format. + #[serde(default)] + pub managed_deny: Option, } /// ADR 0118: one app's resolved public address, as the egress proxy needs it. diff --git a/crates/engram-core/src/types/integration.rs b/crates/engram-core/src/types/integration.rs index 120371d8e..1c208213a 100644 --- a/crates/engram-core/src/types/integration.rs +++ b/crates/engram-core/src/types/integration.rs @@ -27,6 +27,9 @@ use serde::{Deserialize, Serialize}; /// policies persisted before 0057 still parse. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct IntegrationPolicy { + /// Mandatory exact-host denies. Retained across all policy merges. + #[serde(default)] + pub managed_deny: Option, /// Plane-B credential injections selected by the session's capabilities. #[serde(default)] pub injects: Vec, @@ -610,3 +613,23 @@ mod tests { assert_eq!(entry.mint_source, None); } } + +#[cfg(test)] +mod managed_deny_tests { + use super::*; + + #[test] + fn deny_only_policy_survives_json_and_harness_merge() { + let value = serde_json::json!({"managed_deny": crate::types::managed_deny::ManagedDenyPolicy::new(7, ["mcp.example".into()]).unwrap()}); + let policy: IntegrationPolicy = serde_json::from_value(value.clone()).unwrap(); + let merged = merge_harness_egress( + Some(policy), + &crate::types::harness::HarnessEgress { + allow_hosts: vec!["allowed.example".into()], + allow_host_patterns: vec![], + }, + ); + let encoded = serde_json::to_value(&merged).unwrap(); + assert_eq!(encoded.get("managed_deny"), value.get("managed_deny")); + } +} diff --git a/crates/engram-core/src/types/managed_deny.rs b/crates/engram-core/src/types/managed_deny.rs new file mode 100644 index 000000000..543154d5b --- /dev/null +++ b/crates/engram-core/src/types/managed_deny.rs @@ -0,0 +1,296 @@ +//! Exact managed authority policy. The digest matches the canonical MCP envelope. +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ManagedDenyPolicy { + pub version: u32, + pub revision: u64, + pub hosts: Vec, + pub digest: String, +} + +/// Accept DNS names only; normalize the same way for DNS, SNI and HTTP authority. +pub fn canonical_host(raw: &str) -> Option { + let host = raw.strip_suffix('.').unwrap_or(raw).to_ascii_lowercase(); + if host.len() > 253 + || !host.contains('.') + || host.parse::().is_ok() + || !host.split('.').all(|label| { + !label.is_empty() + && label.len() <= 63 + && label.as_bytes()[0].is_ascii_alphanumeric() + && label.as_bytes()[label.len() - 1].is_ascii_alphanumeric() + && label + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-') + }) + { + return None; + } + Some(host) +} + +impl ManagedDenyPolicy { + pub fn new(revision: u64, hosts: impl IntoIterator) -> Result { + if revision > 9_007_199_254_740_991 { + return Err("invalid managed policy revision".into()); + } + let hosts: BTreeSet<_> = hosts + .into_iter() + .map(|h| canonical_host(&h).ok_or_else(|| "invalid managed host".to_string())) + .collect::>()?; + if hosts.len() > 4096 { + return Err("managed host limit exceeded".into()); + } + let mut result = Self { + version: 1, + revision, + hosts: hosts.into_iter().collect(), + digest: String::new(), + }; + // These are canonical key order and integer encoding in the O producer. + let bytes = format!( + "{{\"hosts\":{},\"revision\":{},\"version\":1}}", + serde_json::to_string(&result.hosts).map_err(|e| e.to_string())?, + revision + ); + result.digest = format!("sha256:{}", hex::encode(Sha256::digest(bytes.as_bytes()))); + Ok(result) + } + pub fn validate(&self) -> Result<(), String> { + if Self::new(self.revision, self.hosts.clone())? != *self { + return Err("invalid managed policy digest or canonical hosts".into()); + } + Ok(()) + } + pub fn denies(&self, host: &str) -> bool { + canonical_host(host).is_some_and(|h| self.hosts.binary_search(&h).is_ok()) + } + pub fn contains(&self, old: &Self) -> bool { + old.hosts + .iter() + .all(|h| self.hosts.binary_search(h).is_ok()) + } +} + +/// Physical placement and operator identity are supplied by trusted producers. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ManagedPolicyTarget { + pub session_id: crate::SessionId, + pub sandbox_id: crate::SandboxId, + pub host_id: crate::HostId, + pub binding_epoch: u64, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ManagedPolicyContext { + pub target: ManagedPolicyTarget, + pub operator_generation: u64, + pub operator_identity: String, + /// None is bootstrap, never a final binding acknowledgement. + pub binding_hash: Option, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ManagedNetworkProof { + pub target: ManagedPolicyTarget, + pub guest_ip: std::net::Ipv4Addr, + pub network_identity: String, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ManagedAppliedState { + pub context: ManagedPolicyContext, + pub policy: ManagedDenyPolicy, + pub host_incarnation: String, + pub proxy_incarnation: String, + pub state_id: String, + pub network_proof: Option, +} +impl ManagedAppliedState { + pub fn new( + context: ManagedPolicyContext, + policy: ManagedDenyPolicy, + host_incarnation: String, + proxy_incarnation: String, + network_proof: Option, + ) -> Result { + policy.validate()?; + context.validate()?; + if host_incarnation.is_empty() + || host_incarnation.len() > 128 + || proxy_incarnation.is_empty() + || proxy_incarnation.len() > 128 + { + return Err("missing managed service incarnation".into()); + } + if context.binding_hash.is_some() && network_proof.is_none() { + return Err("final policy requires strict network proof".into()); + } + if network_proof.as_ref().is_some_and(|p| { + p.target != context.target + || p.guest_ip.is_unspecified() + || !is_digest(&p.network_identity) + }) { + return Err("invalid strict network proof".into()); + } + let mut state = Self { + context, + policy, + host_incarnation, + proxy_incarnation, + state_id: String::new(), + network_proof, + }; + state.state_id = format!( + "sha256:{}", + hex::encode(Sha256::digest( + serde_json::to_vec(&state).map_err(|e| e.to_string())? + )) + ); + Ok(state) + } + pub fn validate(&self) -> Result<(), String> { + if Self::new( + self.context.clone(), + self.policy.clone(), + self.host_incarnation.clone(), + self.proxy_incarnation.clone(), + self.network_proof.clone(), + )? != *self + { + return Err("invalid managed receipt".into()); + } + Ok(()) + } +} +impl ManagedPolicyContext { + pub fn validate(&self) -> Result<(), String> { + let t = &self.target; + if t.session_id.as_uuid().is_nil() + || t.sandbox_id.as_uuid().is_nil() + || t.host_id.as_uuid().is_nil() + || t.binding_epoch == 0 + || t.binding_epoch > 9_007_199_254_740_991 + || (self.operator_generation == 0 || self.operator_generation > 9_007_199_254_740_991) + || !is_digest(&self.operator_identity) + || self.binding_hash.as_ref().is_some_and(|h| !is_digest(h)) + { + return Err("invalid managed context".into()); + } + Ok(()) + } +} +pub fn is_digest(value: &str) -> bool { + value.strip_prefix("sha256:").is_some_and(|s| { + s.len() == 64 + && s.bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) + }) +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ManagedPolicyApply { + pub expected: Option, + pub context: ManagedPolicyContext, + pub egress: crate::types::egress::SessionEgressPolicy, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ManagedProxyState { + pub incarnation: String, + pub session_id: crate::SessionId, + pub sandbox_id: crate::SandboxId, + pub guest_ip: std::net::Ipv4Addr, + pub policy: ManagedDenyPolicy, +} + +/// Retain the policy in its existing JSON document, including unrelated fields. +pub fn retain_in_policy_json( + old: Option<&str>, + next: &ManagedDenyPolicy, +) -> Result { + next.validate()?; + let mut value: serde_json::Value = match old { + Some(s) => serde_json::from_str(s).map_err(|e| e.to_string())?, + None => serde_json::json!({}), + }; + let object = value.as_object_mut().ok_or("invalid integration policy")?; + if let Some(old) = object.get("managed_deny").filter(|v| !v.is_null()) { + let old: ManagedDenyPolicy = + serde_json::from_value(old.clone()).map_err(|e| e.to_string())?; + old.validate()?; + if next.revision < old.revision + || !next.contains(&old) + || (next.revision == old.revision && next != &old) + { + return Err("managed policy regressed".into()); + } + } + object.insert( + "managed_deny".into(), + serde_json::to_value(next).map_err(|e| e.to_string())?, + ); + serde_json::to_string(&value).map_err(|e| e.to_string()) +} +pub fn validate_policy_replacement(old: Option<&str>, next: &str) -> Result<(), String> { + let old: serde_json::Value = old + .map(serde_json::from_str) + .transpose() + .map_err(|e| e.to_string())? + .unwrap_or(serde_json::Value::Null); + if old.get("managed_deny").is_some_and(|v| !v.is_null()) { + let new: serde_json::Value = serde_json::from_str(next).map_err(|e| e.to_string())?; + let policy = serde_json::from_value( + new.get("managed_deny") + .ok_or("managed policy cannot be removed")? + .clone(), + ) + .map_err(|e| e.to_string())?; + retain_in_policy_json(Some(&old.to_string()), &policy)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn canonical_digest_and_exact_authorities_match_the_control_contract() { + let policy = ManagedDenyPolicy::new( + 7, + ["MCP.EXAMPLE.", "alias.example", "mcp.example"].map(String::from), + ) + .unwrap(); + assert_eq!( + policy.digest, + "sha256:e8052caef31fb8d39292ff014cf58f711b253b326645e2066b5210b3bb49d265" + ); + assert_eq!(policy.hosts, ["alias.example", "mcp.example"]); + assert!(policy.denies("MCP.EXAMPLE.")); + for other in [ + "other.mcp.example", + "mcp.example.other", + "127.0.0.1", + "mcp.example:443", + ] { + assert!(!policy.denies(other)); + } + for invalid in [ + "127.0.0.1", + "::1", + "localhost", + "*.example", + "a..example", + "mcp.example..", + "mcp.example:443", + "mcp.example/", + "mcp.example\n", + ] { + assert!(ManagedDenyPolicy::new(1, [invalid.into()]).is_err()); + } + let mut reordered = policy.clone(); + reordered.hosts.reverse(); + assert!(reordered.validate().is_err()); + let mut changed = policy; + changed.revision += 1; + assert!(changed.validate().is_err()); + } +} diff --git a/crates/engram-core/src/types/mod.rs b/crates/engram-core/src/types/mod.rs index 7f8ba0ac8..7faef372d 100644 --- a/crates/engram-core/src/types/mod.rs +++ b/crates/engram-core/src/types/mod.rs @@ -57,3 +57,5 @@ pub mod teleport; pub use teleport::*; pub mod mcp_policy; + +pub mod managed_deny; diff --git a/crates/engram-dst/tests/exactly_once.rs b/crates/engram-dst/tests/exactly_once.rs index a35c48591..9e7cd7be2 100644 --- a/crates/engram-dst/tests/exactly_once.rs +++ b/crates/engram-dst/tests/exactly_once.rs @@ -166,6 +166,7 @@ fn agent_spec_with_prompt(prompt_id: &str, text: &str) -> AgentSpec { fn egress_policy(session_id: SessionId, sandbox_id: SandboxId) -> SessionEgressPolicy { SessionEgressPolicy { + managed_deny: None, session_id, sandbox_id, guest_ip: std::net::Ipv4Addr::UNSPECIFIED, diff --git a/crates/engram-egress-proto/src/adopt.rs b/crates/engram-egress-proto/src/adopt.rs index 51345ae6d..135d9bf4f 100644 --- a/crates/engram-egress-proto/src/adopt.rs +++ b/crates/engram-egress-proto/src/adopt.rs @@ -161,6 +161,7 @@ mod tests { fn hello() -> HelloInfo { HelloInfo { + incarnation: "test-incarnation".into(), proto_version: crate::PROTO_VERSION, source_fingerprint: Some(FP.into()), proxy_port: 8443, diff --git a/crates/engram-egress-proto/src/lib.rs b/crates/engram-egress-proto/src/lib.rs index 8f316467e..8e3d89abd 100644 --- a/crates/engram-egress-proto/src/lib.rs +++ b/crates/engram-egress-proto/src/lib.rs @@ -44,7 +44,7 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; /// Bump on any wire-incompatible change. A mismatch in `Hello` /// position is never adopted — the caller kills and respawns. -pub const PROTO_VERSION: u32 = 1; +pub const PROTO_VERSION: u32 = 2; /// Frame size cap. `SyncPolicies` carries every live session's policy /// (resolved secrets included), so the cap is generous; both ends are @@ -85,6 +85,9 @@ pub const BUILD_ENV_SOURCE_FINGERPRINT: &str = "ENGRAM_EGRESS_PROXYD_FINGERPRINT /// Host-agent → proxyd. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum ToProxyd { + ListManaged, + ReadManaged(engram_core::SessionId), + QuarantineManaged(engram_core::SessionId), /// First frame on every connection. Hello { proto_version: u32, @@ -118,6 +121,8 @@ pub enum ToProxyd { /// proxyd → host-agent. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum FromProxyd { + ManagedSessions(Vec), + ManagedState(Option), HelloAck(HelloInfo), Ok, Err(String), @@ -141,6 +146,8 @@ pub struct GuestSummary { /// file, is the authority (the file can be stale or torn). #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct HelloInfo { + #[serde(default)] + pub incarnation: String, pub proto_version: u32, /// The build-time source fingerprint, `None` for local builds. pub source_fingerprint: Option, @@ -320,6 +327,7 @@ mod tests { assert_eq!(back, m); } let m = FromProxyd::HelloAck(HelloInfo { + incarnation: "test-incarnation".into(), proto_version: PROTO_VERSION, source_fingerprint: Some("abc123".into()), proxy_port: 8443, diff --git a/crates/engram-egress-proxy/src/connections.rs b/crates/engram-egress-proxy/src/connections.rs new file mode 100644 index 000000000..2da60ac47 --- /dev/null +++ b/crates/engram-egress-proxy/src/connections.rs @@ -0,0 +1,86 @@ +//! A stable session gate. All socket futures must remain owned by the lease. +use parking_lot::Mutex; +use std::{future::Future, sync::Arc}; +use tokio::sync::{watch, Notify}; + +pub(crate) struct Connections { + state: Mutex, + pub(crate) apply: tokio::sync::Mutex<()>, + drained: Notify, +} +struct State { + open: bool, + active: usize, + cancel: watch::Sender, +} +impl Default for Connections { + fn default() -> Self { + Self { + state: Mutex::new(State { + open: true, + active: 0, + cancel: watch::channel(0).0, + }), + apply: tokio::sync::Mutex::new(()), + drained: Notify::new(), + } + } +} +impl Connections { + pub(crate) fn enroll(self: &Arc) -> Option { + let mut state = self.state.lock(); + if !state.open { + return None; + } + state.active += 1; + Some(ConnectionLease { + control: self.clone(), + cancel: state.cancel.subscribe(), + }) + } + pub(crate) fn is_open(&self) -> bool { + self.state.lock().open + } + pub(crate) fn close(&self) { + let mut state = self.state.lock(); + state.open = false; + state.cancel.send_modify(|v| *v = v.wrapping_add(1)); + } + pub(crate) async fn drain(&self) { + loop { + let notified = self.drained.notified(); + tokio::pin!(notified); + notified.as_mut().enable(); + if self.state.lock().active == 0 { + return; + } + notified.await; + } + } + pub(crate) fn open(&self) { + self.state.lock().open = true; + } +} + +pub struct ConnectionLease { + control: Arc, + cancel: watch::Receiver, +} +impl ConnectionLease { + /// Drop the complete socket future before releasing the drain guard. + pub async fn run(mut self, future: F) -> Option { + tokio::select! { biased; + _ = self.cancel.changed() => None, + result = future => Some(result), + } + } +} +impl Drop for ConnectionLease { + fn drop(&mut self) { + let mut state = self.control.state.lock(); + state.active -= 1; + if state.active == 0 { + self.control.drained.notify_waiters(); + } + } +} diff --git a/crates/engram-egress-proxy/src/dns.rs b/crates/engram-egress-proxy/src/dns.rs index 99738feeb..7e347f15b 100644 --- a/crates/engram-egress-proxy/src/dns.rs +++ b/crates/engram-egress-proxy/src/dns.rs @@ -349,6 +349,9 @@ fn decide(registry: &Registry, guest_ip: Ipv4Addr, query: &Message) -> Decision /// /// A credential-exchange host is refused outright, matching `decide()`. fn name_allowed(state: &SessionState, qname: &str) -> bool { + if state.managed_deny.as_ref().is_some_and(|p| p.denies(qname)) { + return false; + } // ADR 0118: the session's OWN apps resolve unconditionally — before the // deny-list and before the allow-list. A sibling app is not an egress // destination at all: the TCP connection never leaves the host, so @@ -417,6 +420,7 @@ mod tests { #[test] fn allows_exact_match_in_network_allow() { let state = SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: Ipv4Addr::new(10, 200, 0, 2), @@ -452,6 +456,7 @@ mod tests { cred: crate::registry::RefreshableCred::new("token".into(), None), }; let state = SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: Ipv4Addr::new(10, 200, 0, 2), @@ -475,6 +480,7 @@ mod tests { #[test] fn refuses_to_resolve_a_credential_exchange_host() { let state = SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: Ipv4Addr::new(10, 200, 0, 2), @@ -503,6 +509,7 @@ mod tests { #[test] fn allows_wildcard_pattern() { let state = SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: Ipv4Addr::new(10, 200, 0, 2), @@ -536,6 +543,7 @@ mod tests { #[test] fn denies_name_not_in_allow_list() { let state = SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: Ipv4Addr::new(10, 200, 0, 2), @@ -576,6 +584,7 @@ mod tests { // network_allow) should still resolve, mirroring the TCP // path's `Intercept` decision. let state = SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: Ipv4Addr::new(10, 200, 0, 2), @@ -634,6 +643,7 @@ mod tests { panic!("client must be v4"); }; registry.register(SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: client_ip, @@ -675,6 +685,7 @@ mod tests { panic!("client must be v4"); }; registry.register(SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: client_ip, diff --git a/crates/engram-egress-proxy/src/guest_gateway.rs b/crates/engram-egress-proxy/src/guest_gateway.rs index 5b23d59e0..f8eed76a9 100644 --- a/crates/engram-egress-proxy/src/guest_gateway.rs +++ b/crates/engram-egress-proxy/src/guest_gateway.rs @@ -149,10 +149,10 @@ async fn serve_connection( gateway: &GuestGatewayRegistry, ) -> std::io::Result<()> { let session = match peer.ip() { - std::net::IpAddr::V4(ip) => registry.lookup(ip), + std::net::IpAddr::V4(ip) => registry.enroll(ip), std::net::IpAddr::V6(_) => None, }; - let Some(session) = session else { + let Some((session, lease)) = session else { // No registered session owns this guest address. write_response( &mut stream, @@ -165,6 +165,17 @@ async fn serve_connection( return stream.shutdown().await; }; + lease + .run(serve_enrolled(stream, peer, session, gateway)) + .await + .unwrap_or(Ok(())) +} +async fn serve_enrolled( + mut stream: TcpStream, + peer: SocketAddr, + session: Arc, + gateway: &GuestGatewayRegistry, +) -> std::io::Result<()> { let (request, initial_data) = read_request_header(&mut stream, REQUEST_HEADER_TIMEOUT).await?; let request = String::from_utf8_lossy(&request); let mut lines = request.lines(); @@ -665,6 +676,7 @@ mod tests { let registry = Registry::new(); let guest_ip = "10.200.0.2".parse().unwrap(); let state = |guest_services| SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip, @@ -692,6 +704,7 @@ mod tests { async fn socket_endpoint_returns_only_the_placeholder_to_an_enabled_session() { let registry = Arc::new(Registry::new()); registry.register(SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: std::net::Ipv4Addr::LOCALHOST, @@ -736,6 +749,7 @@ mod tests { let registry = Arc::new(Registry::new()); registry.register(SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: std::net::Ipv4Addr::LOCALHOST, @@ -799,6 +813,7 @@ mod tests { async fn connect_preserves_bytes_coalesced_after_the_request_header() { let registry = Arc::new(Registry::new()); registry.register(SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: std::net::Ipv4Addr::LOCALHOST, @@ -854,6 +869,7 @@ mod tests { async fn tunnel_connect_failure_returns_an_actionable_bad_gateway() { let registry = Arc::new(Registry::new()); registry.register(SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: std::net::Ipv4Addr::LOCALHOST, @@ -941,6 +957,7 @@ mod tests { use engram_core::types::integration::CredentialMintSource; let session = SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: std::net::Ipv4Addr::LOCALHOST, @@ -984,6 +1001,7 @@ mod tests { let registry = Arc::new(Registry::new()); registry.register(SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: std::net::Ipv4Addr::LOCALHOST, @@ -1051,3 +1069,96 @@ mod tests { .exists()); } } + +#[cfg(test)] +mod managed_tests { + use super::*; + struct LoopbackTunnel(SocketAddr); + #[async_trait] + impl TunnelUpstream for LoopbackTunnel { + fn kind(&self) -> &'static str { + "test.loopback" + } + async fn connect( + &self, + _: SessionId, + _: &SessionTunnel, + ) -> std::io::Result> { + Ok(Box::new(TcpStream::connect(self.0).await?)) + } + } + #[tokio::test] + async fn managed_transition_drains_gateway_and_refuses_opaque_tunnels() { + tokio::time::timeout(Duration::from_secs(3), async { + let registry = Arc::new(Registry::new()); + let old = SessionState { + managed_deny: None, + session_id: SessionId::new(), + sandbox_id: engram_core::SandboxId::new(), + guest_ip: std::net::Ipv4Addr::LOCALHOST, + network_allow: crate::HostList::empty(), + allow_all: false, + secrets: vec![], + injects: vec![], + observes: vec![], + guest_services: vec![], + apps: vec![], + tunnels: vec![SessionTunnel { + id: "echo".into(), + connector: "test.loopback".into(), + config_json: "{}".into(), + mint_source: None, + }], + }; + registry.register(old.clone()); + let retained = registry.lookup(old.guest_ip).unwrap(); + let upstream = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let gateway = GuestGatewayRegistry::new( + std::iter::empty::>(), + [Arc::new(LoopbackTunnel(upstream.local_addr().unwrap())) + as Arc], + ); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let mut guest = TcpStream::connect(listener.local_addr().unwrap()) + .await + .unwrap(); + let (stream, peer) = listener.accept().await.unwrap(); + let run_registry = registry.clone(); + let task = tokio::spawn(async move { + serve_connection(stream, peer, &run_registry, &gateway).await + }); + guest + .write_all( + b"CONNECT /_engrams/v1/tunnels/echo HTTP/1.1\r\nEngram-Gateway: 1\r\n\r\n", + ) + .await + .unwrap(); + let mut response = [0; 256]; + let n = guest.read(&mut response).await.unwrap(); + assert!(response[..n].starts_with(b"HTTP/1.1 200")); + let (mut remote, _) = upstream.accept().await.unwrap(); + let mut next = old.clone(); + next.managed_deny = Some( + engram_core::types::managed_deny::ManagedDenyPolicy::new(1, ["mcp.example".into()]) + .unwrap(), + ); + assert_eq!( + registry.apply(next.clone()).await.unwrap_err(), + "managed policy does not support tunnels" + ); + next.tunnels.clear(); + registry.apply(next).await.unwrap(); + task.await.unwrap().unwrap(); + assert_eq!(guest.read(&mut [0]).await.unwrap(), 0); + assert_eq!(remote.read(&mut [0]).await.unwrap(), 0); + assert_eq!(retained.tunnels.len(), 1); + registry.unregister(old.session_id); + assert!( + registry.apply(old).await.is_err(), + "remove cannot erase the managed floor" + ); + }) + .await + .expect("bounded gateway drain test"); + } +} diff --git a/crates/engram-egress-proxy/src/intercept.rs b/crates/engram-egress-proxy/src/intercept.rs index 35d72a9ae..c99ce5e2a 100644 --- a/crates/engram-egress-proxy/src/intercept.rs +++ b/crates/engram-egress-proxy/src/intercept.rs @@ -259,6 +259,7 @@ impl rustls::server::ResolvesServerCert for SniResolver { /// protocol and miss the other. They used to carry two hand-copied versions of /// the gate, which is how HTTP/2 kept its own drift. pub struct StreamContext<'a> { + pub strict_authority: bool, pub sni: &'a str, pub port: u16, /// Secrets whose `allow` list covers this host, so their placeholders are @@ -296,6 +297,7 @@ where C: AsyncRead + AsyncWrite + Unpin + Send + 'static, { let StreamContext { + strict_authority, sni, port, secrets, @@ -351,6 +353,7 @@ where client_tls, upstream_tls, StreamContext { + strict_authority, sni, port, secrets, @@ -365,6 +368,7 @@ where .await; } let context = StreamContext { + strict_authority, sni, port, secrets, @@ -385,6 +389,16 @@ where let mut prefix = Vec::with_capacity(8192); let mut head_end = read_request_head(&mut client_tls, &mut prefix).await?; validate_header_block(&prefix[..head_end])?; + if strict_authority { + validate_managed_http1_authority(&prefix[..head_end], sni, port)?; + // Chunked/upgrade protocols cannot use the one-request adapter safely. + if matches!( + request_body_framing(&prefix[..head_end]), + RequestBodyFraming::Chunked + ) { + return Err(InterceptError::MalformedRequest); + } + } // Policy and upstream TLS identity are selected by SNI. Bind the cleartext // HTTP authority to that same name before any credential is attached. @@ -447,8 +461,17 @@ where placeholder: placeholder.to_string(), }); } - prefix = substitute_request(&mut client_tls, prefix, sni, secrets).await?; + prefix = if strict_authority { + substitute_managed_request(&mut client_tls, prefix, sni, secrets).await? + } else { + substitute_request(&mut client_tls, prefix, sni, secrets).await? + }; + let request_remaining = if strict_authority { + Some(bound_managed_request(&mut prefix)?) + } else { + None + }; if !firing.is_empty() { let sink = sink.expect("firing observes imply a sink"); // Force identity encoding + a single, close-delimited response so the @@ -463,6 +486,7 @@ where OBSERVE_RESPONSE_BUDGET, &response_redactions, head_request, + request_remaining, ) .await?; let parsed = observe::parse_response(&resp_buf); @@ -496,7 +520,7 @@ where // bytes-after-prefix (no further substitution — a request body mid-stream // is fine, a second request dies with the connection), upstream→client // is everything. - let (mut client_read, mut client_write) = tokio::io::split(client_tls); + let (client_read, mut client_write) = tokio::io::split(client_tls); let (mut upstream_read, mut upstream_write) = tokio::io::split(upstream_tls); // The gated request is already written and checked above. What remains is a // body that continues past the buffered head, and it is best-effort: we @@ -511,7 +535,11 @@ where // until it has the whole body would otherwise deadlock against a guest that // waits for the response before finishing its own write. let request = async { - let _ = tokio::io::copy(&mut client_read, &mut upstream_write).await; + let _ = tokio::io::copy( + &mut client_read.take(request_remaining.unwrap_or(u64::MAX)), + &mut upstream_write, + ) + .await; let _ = upstream_write.shutdown().await; }; // The response half keeps failing closed: it carries the redaction and the @@ -563,12 +591,13 @@ where .initial_connection_window_size(H2_CONNECTION_WINDOW) .handshake(upstream_tls) .await?; - let upstream = tokio::spawn(connection); + tokio::pin!(connection); let mut active = FuturesUnordered::new(); let mut accepting = true; while accepting || !active.is_empty() { tokio::select! { + result = &mut connection => { result?; break; } incoming = inbound.accept(), if accepting => { match incoming { Some(Ok((request, respond))) => { @@ -596,8 +625,6 @@ where } } - upstream.abort(); - let _ = upstream.await; Ok(()) } @@ -607,6 +634,24 @@ async fn process_h2_stream( mut outbound: h2::client::SendRequest, context: &StreamContext<'_>, ) -> Result<(), InterceptError> { + if context.strict_authority + && (request.method() == http::Method::CONNECT + || !request + .uri() + .authority() + .is_some_and(|a| managed_authority_matches(a.as_str(), context.sni, context.port)) + || request + .headers() + .get_all(http::header::HOST) + .iter() + .any(|h| { + !h.to_str() + .is_ok_and(|h| managed_authority_matches(h, context.sni, context.port)) + })) + { + deny_h2(&mut respond, http::StatusCode::FORBIDDEN)?; + return Err(InterceptError::MalformedRequest); + } let method = request.method().as_str().to_string(); let path = request .uri() @@ -1444,6 +1489,18 @@ where Ok(out) } +/// Bound the original framing before substitution changes Content-Length. +/// The limited reader also prevents body buffering from consuming a next request. +async fn substitute_managed_request( + client: &mut C, + mut prefix: Vec, + sni: &str, + secrets: &[&SecretEntry], +) -> Result, InterceptError> { + let remaining = bound_managed_request(&mut prefix)?; + substitute_request(&mut client.take(remaining), prefix, sni, secrets).await +} + /// Rewrite the request's `Content-Length` to `length`. The header block is /// already validated, so exactly one `Content-Length` is present. fn rewrite_content_length(head: &[u8], length: usize) -> Result, InterceptError> { @@ -2160,6 +2217,7 @@ async fn pump_and_observe( budget: usize, response_redactions: &[Vec], head_request: bool, + request_remaining: Option, ) -> Result, InterceptError> where C: AsyncRead + AsyncWrite + Unpin + Send + 'static, @@ -2168,16 +2226,19 @@ where let (client_rd, mut client_wr) = tokio::io::split(client_tls); let (mut up_rd, up_wr) = tokio::io::split(upstream_tls); - // Detached: drain any remaining request body to upstream. Aborted once the - // response is in — by then upstream has the full request (it responded). - let c2u = tokio::spawn(async move { - let mut client_rd = client_rd; + // Own both socket halves until the response completes or the session cancels. + let c2u = async move { let mut up_wr = up_wr; - let _ = tokio::io::copy(&mut client_rd, &mut up_wr).await; + let _ = tokio::io::copy( + &mut client_rd.take(request_remaining.unwrap_or(u64::MAX)), + &mut up_wr, + ) + .await; let _ = up_wr.shutdown().await; - }); + }; + tokio::pin!(c2u); - let response_result = async { + let response = async { let mut resp_buf = Vec::new(); let mut redactor = Http1ResponseRedactor::new(response_redactions, head_request); if !response_redactions.is_empty() { @@ -2235,11 +2296,66 @@ where let _ = client_wr.flush().await; let _ = client_wr.shutdown().await; Ok::<_, InterceptError>(resp_buf) + }; + tokio::pin!(response); + tokio::select! { + result = &mut response => result, + _ = &mut c2u => response.await, + } +} + +fn managed_authority_matches(raw: &str, sni: &str, port: u16) -> bool { + let Ok(authority) = raw.parse::() else { + return false; + }; + if raw.contains('@') || authority.port_u16().unwrap_or(443) != port { + return false; + } + let host = engram_core::types::managed_deny::canonical_host(authority.host()); + host.is_some() && host == engram_core::types::managed_deny::canonical_host(sni) +} +fn validate_managed_http1_authority( + head: &[u8], + sni: &str, + port: u16, +) -> Result<(), InterceptError> { + let text = std::str::from_utf8(head).map_err(|_| InterceptError::MalformedRequest)?; + let mut lines = text.split("\r\n"); + if lines.next().is_some_and(|l| l.starts_with("CONNECT ")) { + return Err(InterceptError::MalformedRequest); } - .await; - c2u.abort(); - let _ = c2u.await; - response_result + let mut hosts = 0; + for line in lines { + if let Some((name, value)) = line.split_once(':') { + if name.eq_ignore_ascii_case("host") { + hosts += 1; + if !managed_authority_matches(value.trim(), sni, port) { + return Err(InterceptError::MalformedRequest); + } + } + if name.eq_ignore_ascii_case("upgrade") { + return Err(InterceptError::MalformedRequest); + } + } + } + if hosts != 1 { + return Err(InterceptError::MalformedRequest); + } + Ok(()) +} +/// Keep only this framed request; never forward a pipelined next request unchecked. +fn bound_managed_request(prefix: &mut Vec) -> Result { + let head_end = head_end_of(prefix).ok_or(InterceptError::MalformedRequest)?; + let body = match request_body_framing(&prefix[..head_end]) { + RequestBodyFraming::None => 0, + RequestBodyFraming::ContentLength(n) => n, + RequestBodyFraming::Chunked => return Err(InterceptError::MalformedRequest), + }; + let end = head_end + .checked_add(body) + .ok_or(InterceptError::MalformedRequest)?; + prefix.truncate(end); + Ok((end - prefix.len()) as u64) } #[cfg(test)] @@ -2523,6 +2639,41 @@ mod hardening_tests { assert_eq!(out, request); } + #[tokio::test] + async fn managed_substitution_does_not_fold_a_pipelined_request_into_the_body() { + let secret = placeholder_secret(); + let body = b"engram_ph_x"; + let head = format!( + "POST / HTTP/1.1\r\nHost: api.example\r\nContent-Length: {}\r\n\r\n", + body.len() + ) + .into_bytes(); + let next = b"GET /other HTTP/1.1\r\nHost: mcp.example\r\n\r\n"; + for buffered in [false, true] { + let mut prefix = head.clone(); + let mut unread = Vec::new(); + if buffered { + prefix.extend_from_slice(body); + prefix.extend_from_slice(next); + } else { + unread.extend_from_slice(body); + unread.extend_from_slice(next); + } + let out = substitute_managed_request( + &mut unread.as_slice(), + prefix, + "api.example", + &[&secret], + ) + .await + .unwrap(); + let text = String::from_utf8(out).unwrap(); + assert!(text.ends_with("sk-a-much-longer-real-value")); + assert!(!text.contains("/other") && !text.contains("mcp.example")); + assert!(text.contains("Content-Length: 27\r\n")); + } + } + #[test] fn binds_authority_to_sni_and_removes_duplicates() { let request = b"GET /v1 HTTP/1.1\r\nHost: allowed.example\r\nhOsT: attacker.example\r\nAccept: */*\r\n\r\n".to_vec(); @@ -2672,6 +2823,7 @@ mod hardening_tests { proxy_guest_io, proxy_upstream_io, StreamContext { + strict_authority: false, sni: "api.example", port: 443, secrets: &[&secret], diff --git a/crates/engram-egress-proxy/src/lib.rs b/crates/engram-egress-proxy/src/lib.rs index 915930270..14b259c33 100644 --- a/crates/engram-egress-proxy/src/lib.rs +++ b/crates/engram-egress-proxy/src/lib.rs @@ -60,3 +60,5 @@ pub use sni::{peek_sni, PeekError as SniPeekError}; pub use substitute::{scan_for_violation, substitute}; pub use tunnel_pool::{EndpointFactory, PoolConfig, PooledConn, TunnelEndpoint, TunnelPool}; pub use violation::first_match as scan_for_placeholder; + +mod connections; diff --git a/crates/engram-egress-proxy/src/proxy.rs b/crates/engram-egress-proxy/src/proxy.rs index 226b97ef5..9a0c0df79 100644 --- a/crates/engram-egress-proxy/src/proxy.rs +++ b/crates/engram-egress-proxy/src/proxy.rs @@ -224,19 +224,26 @@ impl Proxy { let observe_sink = self.cfg.observe_sink.clone(); let inject_refresher = self.cfg.inject_refresher.clone(); let guest_port_dialer = self.cfg.guest_port_dialer.clone(); + let SocketAddr::V4(peer_v4) = peer else { + continue; + }; + let Some((session, lease)) = registry.enroll(*peer_v4.ip()) else { + continue; + }; tokio::spawn(async move { - if let Err(e) = handle( - stream, - peer, - registry, - resolver, - server_cfg, - client_cfg, - observe_sink, - inject_refresher, - guest_port_dialer, - ) - .await + if let Some(Err(e)) = lease + .run(handle( + stream, + peer, + session, + resolver, + server_cfg, + client_cfg, + observe_sink, + inject_refresher, + guest_port_dialer, + )) + .await { tracing::debug!(peer = %peer, error = %e, "connection handler ended with error"); } @@ -257,8 +264,8 @@ pub struct Listeners { #[allow(clippy::too_many_arguments)] async fn handle( mut stream: TcpStream, - peer: SocketAddr, - registry: Arc, + _peer: SocketAddr, + session: Arc, resolver: Arc, server_cfg: Arc, client_cfg: Arc, @@ -266,12 +273,6 @@ async fn handle( inject_refresher: Option>, guest_port_dialer: Option, ) -> Result<(), HandleError> { - let guest_ip = match peer.ip() { - IpAddr::V4(v4) => v4, - IpAddr::V6(_) => return Err(HandleError::NoSession), - }; - let session = registry.lookup(guest_ip).ok_or(HandleError::NoSession)?; - // Pre-REDIRECT destination — useful only for logs. We don't // dial it; the upstream is resolved fresh by SNI on the host. // On non-Linux this errors (no SO_ORIGINAL_DST) — fine, we @@ -372,6 +373,7 @@ async fn handle( peeked, resolver, intercept::StreamContext { + strict_authority: session.managed_deny.is_some(), sni: &sni, port, secrets: &secrets, @@ -438,7 +440,6 @@ async fn handle( #[derive(Debug)] enum HandleError { - NoSession, SniPeek(crate::sni::PeekError), Bypass(crate::bypass::BypassError), Intercept(crate::intercept::InterceptError), @@ -450,7 +451,6 @@ enum HandleError { impl std::fmt::Display for HandleError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::NoSession => write!(f, "no session for source IP"), Self::SniPeek(e) => write!(f, "sni peek: {e}"), Self::Bypass(e) => write!(f, "bypass: {e}"), Self::Intercept(e) => write!(f, "intercept: {e}"), diff --git a/crates/engram-egress-proxy/src/registry.rs b/crates/engram-egress-proxy/src/registry.rs index 5a15a5293..083179f1a 100644 --- a/crates/engram-egress-proxy/src/registry.rs +++ b/crates/engram-egress-proxy/src/registry.rs @@ -27,6 +27,7 @@ use crate::policy::HostList; /// arriving from a given guest IP. #[derive(Clone, Debug)] pub struct SessionState { + pub managed_deny: Option, pub session_id: SessionId, /// The sandbox this session is bound to. Carried so the ADR 0118 short /// circuit can dial a guest port without a session→sandbox lookup — the @@ -474,19 +475,43 @@ pub struct Registry { struct RegistryInner { by_guest_ip: HashMap>, by_session: HashMap, + controls: HashMap>, + managed_floor: HashMap, } impl Registry { + pub fn managed_sessions(&self) -> Vec { + let state = self.inner.read(); + state + .by_session + .keys() + .filter(|id| state.managed_floor.contains_key(id)) + .copied() + .collect() + } pub fn new() -> Self { Self::default() } /// Replaces any prior registration for this `session_id`. pub fn register(&self, state: SessionState) { + // Synchronous registration is for an initial/ordinary policy. Managed + // replacement must use apply, which closes and drains old sockets. + let mut g = self.inner.write(); + if state.managed_deny.is_some() || g.managed_floor.contains_key(&state.session_id) { + return; + } + Self::publish(&mut g, state); + } + + fn publish(g: &mut RegistryInner, state: SessionState) { let guest_ip = state.guest_ip; let session_id = state.session_id; + if let Some(policy) = &state.managed_deny { + g.managed_floor.insert(session_id, policy.clone()); + } let arc = Arc::new(state); - let mut g = self.inner.write(); + g.controls.entry(session_id).or_default(); // Remove any stale guest_ip mapping if the session moved // (shouldn't happen — sandbox ids are stable — but defensive). if let Some(old_ip) = g.by_session.insert(session_id, guest_ip) { @@ -497,15 +522,109 @@ impl Registry { g.by_guest_ip.insert(guest_ip, arc); } + pub fn enroll( + &self, + guest_ip: Ipv4Addr, + ) -> Option<(Arc, crate::connections::ConnectionLease)> { + let g = self.inner.read(); + let state = g.by_guest_ip.get(&guest_ip)?.clone(); + let lease = g.controls.get(&state.session_id)?.enroll()?; + Some((state, lease)) + } + + pub async fn apply(&self, state: SessionState) -> Result<(), String> { + if let Some(policy) = &state.managed_deny { + policy.validate()?; + if !state.tunnels.is_empty() { + return Err("managed policy does not support tunnels".into()); + } + if state.guest_ip.is_unspecified() + || state.guest_ip.is_multicast() + || state.session_id.as_uuid().is_nil() + || state.sandbox_id.as_uuid().is_nil() + { + return Err("managed policy has no guest identity".into()); + } + } + let control = self + .inner + .write() + .controls + .entry(state.session_id) + .or_default() + .clone(); + let _apply = control.apply.lock().await; + { + let g = self.inner.read(); + if let Some(old) = g.managed_floor.get(&state.session_id) { + let new = state + .managed_deny + .as_ref() + .ok_or("managed policy cannot be removed")?; + if new.revision < old.revision + || !new.contains(old) + || (new.revision == old.revision && new != old) + { + return Err("managed policy regressed".into()); + } + } + } + if state.managed_deny.is_some() { + control.close(); + control.drain().await; + } + Self::publish(&mut self.inner.write(), state); + control.open(); + Ok(()) + } + + pub async fn quarantine_and_drain(&self, session_id: SessionId) { + let control = self + .inner + .write() + .controls + .entry(session_id) + .or_default() + .clone(); + let _apply = control.apply.lock().await; + control.close(); + control.drain().await; + } + pub fn lookup_session(&self, session_id: SessionId) -> Option> { + let g = self.inner.read(); + let ip = g.by_session.get(&session_id)?; + g.controls + .get(&session_id)? + .is_open() + .then(|| g.by_guest_ip.get(ip).cloned()) + .flatten() + } + pub fn quarantine(&self, session_id: SessionId) { + self.inner + .write() + .controls + .entry(session_id) + .or_default() + .close(); + } + pub fn unregister(&self, session_id: SessionId) { let mut g = self.inner.write(); + if let Some(control) = g.controls.get(&session_id) { + control.close(); + } if let Some(ip) = g.by_session.remove(&session_id) { g.by_guest_ip.remove(&ip); } } pub fn lookup(&self, guest_ip: Ipv4Addr) -> Option> { - self.inner.read().by_guest_ip.get(&guest_ip).cloned() + let g = self.inner.read(); + let state = g.by_guest_ip.get(&guest_ip)?; + g.controls + .get(&state.session_id)? + .is_open() + .then(|| state.clone()) } pub fn live_count(&self) -> usize { @@ -533,6 +652,13 @@ impl SessionState { /// straight through — the guest could trade its session token for a /// credential this proxy no longer bounds. pub fn decide(&self, hostname: &str) -> Decision<'_> { + if self + .managed_deny + .as_ref() + .is_some_and(|p| p.denies(hostname)) + { + return Decision::Reject; + } // ADR 0118: checked FIRST, before the deny-list and the allow-list. A // sibling app is not an egress destination — the connection is spliced // back into this same sandbox and never leaves the host — so it is not @@ -573,7 +699,8 @@ impl SessionState { // spliced: the rule reads the request line, which only an // intercepted connection produces. Interception with an empty // policy relays the connection as before and lets the rule run. - if crate::google_denylist::requires_inspection(hostname) { + if self.managed_deny.is_some() || crate::google_denylist::requires_inspection(hostname) + { return Decision::Intercept { secrets: Vec::new(), injects: Vec::new(), @@ -649,8 +776,121 @@ mod tests { use super::*; use std::str::FromStr; + #[test] + fn managed_denies_win_over_every_existing_rule() { + let mut s = state(); + s.allow_all = true; + s.apps.push(AppEndpoint { + hostname: "api.openai.com".into(), + port: 8080, + }); + s.managed_deny = Some( + engram_core::types::managed_deny::ManagedDenyPolicy::new( + 1, + [ + "api.openai.com", + "api.datadoghq.com", + "api.github.com", + "registry.npmjs.org", + "alias.example", + ] + .map(String::from), + ) + .unwrap(), + ); + for host in [ + "API.OPENAI.COM.", + "api.datadoghq.com", + "api.github.com", + "registry.npmjs.org", + "alias.example", + ] { + assert!(matches!(s.decide(host), Decision::Reject), "{host}"); + } + assert!(matches!( + s.decide("near.alias.example"), + Decision::Intercept { .. } + )); + } + + #[tokio::test] + async fn managed_apply_drains_old_arc_sockets_before_return() { + use tokio::io::AsyncReadExt; + let registry = Arc::new(Registry::new()); + let initial = state(); + let ip = initial.guest_ip; + let mut next = initial.clone(); + registry.register(initial); + let retained = registry.lookup(ip).unwrap(); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let mut peer = tokio::net::TcpStream::connect(listener.local_addr().unwrap()) + .await + .unwrap(); + let (mut socket, _) = listener.accept().await.unwrap(); + let (_, lease) = registry.enroll(ip).unwrap(); + let task = tokio::spawn(lease.run(async move { + let mut b = [0]; + let _ = socket.read(&mut b).await; + })); + next.managed_deny = Some( + engram_core::types::managed_deny::ManagedDenyPolicy::new(1, ["api.openai.com".into()]) + .unwrap(), + ); + tokio::time::timeout(std::time::Duration::from_secs(2), async { + registry.apply(next).await.unwrap(); + task.await.unwrap(); + assert_eq!(peer.read(&mut [0]).await.unwrap(), 0); + }) + .await + .expect("socket drain is bounded"); + assert!(matches!( + retained.decide("api.openai.com"), + Decision::Intercept { .. } + )); + assert!(matches!( + registry.lookup(ip).unwrap().decide("api.openai.com"), + Decision::Reject + )); + } + + #[tokio::test] + async fn managed_drain_does_not_lock_other_sessions() { + tokio::time::timeout(std::time::Duration::from_secs(2), async { + let registry = Arc::new(Registry::new()); + let mut first = state(); + let mut second = state(); + second.guest_ip = "10.200.0.3".parse().unwrap(); + registry.register(first.clone()); + registry.register(second.clone()); + let (_, held) = registry.enroll(first.guest_ip).unwrap(); + first.managed_deny = Some( + engram_core::types::managed_deny::ManagedDenyPolicy::new(1, ["mcp.example".into()]) + .unwrap(), + ); + second.managed_deny = first.managed_deny.clone(); + let first_ip = first.guest_ip; + let other_ip = second.guest_ip; + let worker = registry.clone(); + let applying = tokio::spawn(async move { worker.apply(first).await }); + while registry.lookup(first_ip).is_some() { + tokio::task::yield_now().await; + } + registry.apply(second).await.unwrap(); + assert!(registry.lookup(other_ip).is_some()); + assert!( + !applying.is_finished(), + "first drain must still wait for its lease" + ); + drop(held); + applying.await.unwrap().unwrap(); + }) + .await + .expect("per-session drain has no global wait"); + } + fn state() -> SessionState { SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: Ipv4Addr::from_str("10.200.0.2").unwrap(), diff --git a/crates/engram-egress-proxy/tests/intercept_e2e.rs b/crates/engram-egress-proxy/tests/intercept_e2e.rs index b80f29930..5641baf67 100644 --- a/crates/engram-egress-proxy/tests/intercept_e2e.rs +++ b/crates/engram-egress-proxy/tests/intercept_e2e.rs @@ -240,6 +240,7 @@ async fn substitutes_placeholder_in_intercept_path() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &secrets, @@ -335,6 +336,7 @@ async fn violation_returned_when_placeholder_targets_disallowed_host() { // this host disallow?" always got an empty answer. The detector could not // fire in production, and this test could not notice. let session = SessionState { + managed_deny: None, session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: "10.200.0.2".parse().unwrap(), @@ -382,6 +384,7 @@ async fn violation_returned_when_placeholder_targets_disallowed_host() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &secrets, @@ -462,6 +465,7 @@ async fn injects_header_on_allowed_request() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -537,6 +541,7 @@ async fn keep_alive_second_request_cannot_bypass_the_gate() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -662,6 +667,7 @@ async fn guest_upgrade_header_cannot_reopen_the_bypass() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -771,6 +777,7 @@ async fn rejects_request_shape_outside_policy() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -840,6 +847,7 @@ async fn reject_writes_no_response_to_the_guest() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -950,6 +958,7 @@ async fn near_expiry_inject_is_reminted_before_forwarding() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -1120,6 +1129,7 @@ async fn observes_response_and_emits_asset() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -1220,6 +1230,7 @@ async fn failed_status_emits_no_asset() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -1340,6 +1351,7 @@ async fn run_graphql_inject( Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -1574,6 +1586,7 @@ async fn run_graphql_observe( Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -1840,6 +1853,7 @@ async fn h2_intercept_injects_and_redacts_across_headers_body_and_trailers() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "fake-upstream", port: upstream_addr.port(), secrets: &[], @@ -1953,6 +1967,7 @@ async fn h2_intercept_denies_a_grpc_credential_method() { Vec::new(), resolver, intercept::StreamContext { + strict_authority: false, sni: "iam.googleapis.com", port: upstream_addr.port(), secrets: &[], @@ -2014,3 +2029,109 @@ async fn h2_intercept_denies_a_grpc_credential_method() { driver.abort(); let _ = tokio::time::timeout(std::time::Duration::from_secs(5), proxy_task).await; } + +#[tokio::test] +async fn managed_default_allow_rejects_host_override_without_injecting_credentials() { + tokio::time::timeout(std::time::Duration::from_secs(5), async { + for (authority, succeeds) in [("mcp.example", false), ("API.EXAMPLE.", true)] { + let ca = ca(); + let captured = Arc::new(Mutex::new(Vec::new())); + let upstream_addr = fake_upstream(captured.clone()).await; + let mut session = SessionState { + managed_deny: Some(engram_core::types::managed_deny::ManagedDenyPolicy::new(1, ["mcp.example".into()]).unwrap()), + session_id: SessionId::new(), sandbox_id: engram_core::SandboxId::new(), guest_ip: "127.0.0.1".parse().unwrap(), + network_allow: HostList::from_manifest(&[], &[]).unwrap(), allow_all: true, + secrets: vec![], injects: vec![], observes: vec![], guest_services: vec![], tunnels: vec![], apps: vec![], + }; + let Decision::Intercept { secrets, injects, observes, .. } = session.decide("api.example") else { panic!("managed Literal/default-allow TLS must be inspected"); }; + assert!(secrets.is_empty() && injects.is_empty() && observes.is_empty()); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let guest = TcpStream::connect(listener.local_addr().unwrap()).await.unwrap(); + let (proxy, _) = listener.accept().await.unwrap(); + let server_cfg = build_server_config(Arc::new(CertMint::new(ca.clone()))); + let task = tokio::spawn(async move { + intercept::run(proxy, vec![], Arc::new(StaticResolver::new().with("api.example", upstream_addr)), intercept::StreamContext { + strict_authority: true, sni: "api.example", port: 443, secrets: &[], injects: &[], observes: &[], foreign_placeholders: &[], session_id: SessionId::new(), sink: None, refresher: None, + }, server_cfg, build_client_config()).await + }); + let mut roots = rustls::RootCertStore::empty(); + for cert in rustls_pemfile::certs(&mut ca.cert_pem.as_bytes()) { roots.add(cert.unwrap()).unwrap(); } + let connector = TlsConnector::from(Arc::new(rustls::ClientConfig::builder().with_root_certificates(roots).with_no_client_auth())); + let mut guest = connector.connect("api.example".try_into().unwrap(), guest).await.unwrap(); + guest.write_all(format!("GET / HTTP/1.1\r\nHost: {authority}\r\nConnection: close\r\n\r\nGET /other HTTP/1.1\r\nHost: mcp.example\r\n\r\n").as_bytes()).await.unwrap(); + guest.shutdown().await.unwrap(); + let mut response = Vec::new(); let _ = guest.read_to_end(&mut response).await; + let result = task.await.unwrap(); + assert_eq!(result.is_ok(), succeeds); + let request = String::from_utf8_lossy(&captured.lock()).into_owned(); + if succeeds { + assert!(request.contains("Host: api.example\r\n")); + assert!(!request.contains("Authorization") && !request.contains("/other") && !request.contains("mcp.example")); + } else { assert!(request.is_empty()); } + session.managed_deny = None; + assert!(matches!(session.decide("api.example"), Decision::Bypass)); + } + }).await.expect("bounded managed HTTP authority test"); +} + +#[tokio::test] +async fn managed_h2_rejects_authority_override() { + tokio::time::timeout(std::time::Duration::from_secs(5), async { + let ca = ca(); + let upstream = h2_upstream(Arc::new(Mutex::new(None))).await; + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let guest = TcpStream::connect(listener.local_addr().unwrap()) + .await + .unwrap(); + let (proxy, _) = listener.accept().await.unwrap(); + let server = build_server_config(Arc::new(CertMint::new(ca.clone()))); + let task = tokio::spawn(async move { + intercept::run( + proxy, + vec![], + Arc::new(StaticResolver::new().with("api.example", upstream)), + intercept::StreamContext { + strict_authority: true, + sni: "api.example", + port: 443, + secrets: &[], + injects: &[], + observes: &[], + foreign_placeholders: &[], + session_id: SessionId::new(), + sink: None, + refresher: None, + }, + server, + build_client_config(), + ) + .await + }); + let mut roots = rustls::RootCertStore::empty(); + for cert in rustls_pemfile::certs(&mut ca.cert_pem.as_bytes()) { + roots.add(cert.unwrap()).unwrap(); + } + let mut cfg = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth(); + cfg.alpn_protocols = vec![b"h2".to_vec()]; + let guest = TlsConnector::from(Arc::new(cfg)) + .connect("api.example".try_into().unwrap(), guest) + .await + .unwrap(); + let (mut client, connection) = h2::client::handshake(guest).await.unwrap(); + let connection = tokio::spawn(connection); + let request = http::Request::builder() + .uri("https://mcp.example/forbidden") + .body(()) + .unwrap(); + let (response, _) = client.send_request(request, true).unwrap(); + assert_eq!(response.await.unwrap().status(), 403); + drop(client); + connection.abort(); + let _ = connection.await; + let _ = task.await.unwrap(); + }) + .await + .expect("bounded managed H2 authority test"); +} diff --git a/crates/engram-egress-proxyd/src/control.rs b/crates/engram-egress-proxyd/src/control.rs index 0f4bc1bd1..d9d1fce12 100644 --- a/crates/engram-egress-proxyd/src/control.rs +++ b/crates/engram-egress-proxyd/src/control.rs @@ -57,6 +57,24 @@ async fn serve_conn( loop { let msg: ToProxyd = engram_egress_proto::read_frame(&mut stream).await?; let reply = match msg { + ToProxyd::ListManaged => FromProxyd::ManagedSessions(state.registry.managed_sessions()), + ToProxyd::QuarantineManaged(session) => { + state.registry.quarantine_and_drain(session).await; + FromProxyd::Ok + } + ToProxyd::ReadManaged(session) => { + FromProxyd::ManagedState(state.registry.lookup_session(session).and_then(|s| { + s.managed_deny.clone().map(|policy| { + engram_core::types::managed_deny::ManagedProxyState { + incarnation: state.hello.incarnation.clone(), + session_id: s.session_id, + sandbox_id: s.sandbox_id, + guest_ip: s.guest_ip, + policy, + } + }) + })) + } ToProxyd::Hello { proto_version } => { if proto_version != engram_egress_proto::PROTO_VERSION { tracing::warn!( @@ -70,11 +88,13 @@ async fn serve_conn( // on a mismatch. FromProxyd::HelloAck(state.hello.clone()) } - ToProxyd::SyncPolicies(policies) => sync_policies(&state, policies), - ToProxyd::ApplyPolicy(policy) => match register_policy(&state.registry, *policy) { - Ok(()) => FromProxyd::Ok, - Err(e) => FromProxyd::Err(format!("translate egress policy: {e}")), - }, + ToProxyd::SyncPolicies(policies) => sync_policies(&state, policies).await, + ToProxyd::ApplyPolicy(policy) => { + match register_policy(&state.registry, *policy).await { + Ok(()) => FromProxyd::Ok, + Err(e) => FromProxyd::Err(format!("translate egress policy: {e}")), + } + } ToProxyd::RemoveSession(session_id) => { unregister_session(&state, session_id); FromProxyd::Ok @@ -130,24 +150,27 @@ fn unregister_session(state: &ControlState, session_id: engram_core::SessionId) /// that translated, instead of being pruned into a total egress /// lockout. The per-apply path (`ApplyPolicy`) keeps erroring loudly /// — only the bulk replace shields survivors. -fn sync_policies( +async fn sync_policies( state: &ControlState, policies: Vec, ) -> FromProxyd { let mut keep = std::collections::HashSet::with_capacity(policies.len()); let mut failed = 0usize; + let mut managed_failed = false; let mut applied = 0usize; let total = policies.len(); for policy in policies { let session_id = policy.session_id; + let managed = policy.managed_deny.is_some(); // Shield the session from the prune below regardless of the // translate outcome — a defective UPDATE must not evict a // working registration. keep.insert(session_id); - match register_policy(&state.registry, policy) { + match register_policy(&state.registry, policy).await { Ok(()) => applied += 1, Err(e) => { failed += 1; + managed_failed |= managed; tracing::error!( %session_id, error = %e, @@ -168,7 +191,11 @@ fn sync_policies( pruned += 1; } tracing::info!(applied, failed, pruned, total, "policies synced"); - FromProxyd::Ok + if managed_failed { + FromProxyd::Err("managed policy unavailable; session quarantined".into()) + } else { + FromProxyd::Ok + } } /// Bind the proxy listeners, retrying briefly to ride a transient port @@ -232,19 +259,24 @@ fn graphql_match(op: &str, field: &str) -> Result Result<(), engram_egress_proxy::policy::ParseError> { +) -> Result<(), String> { + if policy.managed_deny.is_some() { + registry.quarantine(policy.session_id); + } let network_allow = engram_egress_proxy::HostList::from_manifest( &policy.network_allow_hosts, &policy.network_allow_host_patterns, - )?; + ) + .map_err(|e| e.to_string())?; let allow_all = policy.allow_all; let mut secrets = Vec::with_capacity(policy.secrets.len()); for s in policy.secrets { let allow = - engram_egress_proxy::HostList::from_manifest(&s.allow_hosts, &s.allow_host_patterns)?; + engram_egress_proxy::HostList::from_manifest(&s.allow_hosts, &s.allow_host_patterns) + .map_err(|e| e.to_string())?; secrets.push(engram_egress_proxy::SecretEntry { placeholder: s.placeholder, real_value: s.real_value, @@ -257,7 +289,8 @@ pub(crate) fn register_policy( let mut injects = Vec::with_capacity(policy.injects.len()); for i in policy.injects { let allow = - engram_egress_proxy::HostList::from_manifest(&i.allow_hosts, &i.allow_host_patterns)?; + engram_egress_proxy::HostList::from_manifest(&i.allow_hosts, &i.allow_host_patterns) + .map_err(|e| e.to_string())?; // ADR 0059: a non-empty-but-unparseable graphql op is a corrupt entry — // skip it (fail-closed) so it never degrades into a permissive REST gate // on `/graphql`. @@ -292,7 +325,8 @@ pub(crate) fn register_policy( let mut observes = Vec::with_capacity(policy.observes.len()); for o in policy.observes { let allow = - engram_egress_proxy::HostList::from_manifest(&o.allow_hosts, &o.allow_host_patterns)?; + engram_egress_proxy::HostList::from_manifest(&o.allow_hosts, &o.allow_host_patterns) + .map_err(|e| e.to_string())?; let graphql = match graphql_match(&o.graphql_operation, &o.graphql_field) { Ok(g) => g, Err(()) => { @@ -332,21 +366,23 @@ pub(crate) fn register_policy( }), }); } - registry.register(engram_egress_proxy::SessionState { - session_id: policy.session_id, - sandbox_id: policy.sandbox_id, - guest_ip: policy.guest_ip, - network_allow, - allow_all, - secrets, - injects, - observes, - guest_services: policy.guest_services, - tunnels: policy.tunnels, - // ADR 0118: the session's own app hostnames, for the SNI short circuit. - apps: policy.apps, - }); - Ok(()) + registry + .apply(engram_egress_proxy::SessionState { + managed_deny: policy.managed_deny, + session_id: policy.session_id, + sandbox_id: policy.sandbox_id, + guest_ip: policy.guest_ip, + network_allow, + allow_all, + secrets, + injects, + observes, + guest_services: policy.guest_services, + tunnels: policy.tunnels, + // ADR 0118: the session's own app hostnames, for the SNI short circuit. + apps: policy.apps, + }) + .await } #[cfg(test)] @@ -359,6 +395,7 @@ mod tests { fn policy(session_id: SessionId, ip: Ipv4Addr) -> SessionEgressPolicy { SessionEgressPolicy { + managed_deny: None, session_id, sandbox_id: SandboxId::new(), guest_ip: ip, @@ -381,6 +418,7 @@ mod tests { gateway: Arc::new(GuestGatewayRegistry::default()), shutdown: tokio::sync::watch::channel(false).0, hello: HelloInfo { + incarnation: "test-incarnation".into(), proto_version: engram_egress_proto::PROTO_VERSION, source_fingerprint: None, proxy_port: 8443, @@ -392,16 +430,18 @@ mod tests { } } - #[test] - fn sync_registers_and_prunes() { + #[tokio::test] + async fn sync_registers_and_prunes() { let state = state(); let kept = SessionId::new(); let stale = SessionId::new(); // A pre-existing registration the sync set does not carry. - register_policy(&state.registry, policy(stale, Ipv4Addr::new(10, 200, 0, 2))).unwrap(); + register_policy(&state.registry, policy(stale, Ipv4Addr::new(10, 200, 0, 2))) + .await + .unwrap(); assert_eq!(state.registry.live_count(), 1); - let reply = sync_policies(&state, vec![policy(kept, Ipv4Addr::new(10, 200, 0, 6))]); + let reply = sync_policies(&state, vec![policy(kept, Ipv4Addr::new(10, 200, 0, 6))]).await; assert_eq!(reply, FromProxyd::Ok); assert_eq!(state.registry.live_count(), 1); assert!(state @@ -414,8 +454,8 @@ mod tests { .is_none()); } - #[test] - fn sync_skips_untranslatable_policies_and_keeps_the_rest() { + #[tokio::test] + async fn sync_skips_untranslatable_policies_and_keeps_the_rest() { let state = state(); let good = SessionId::new(); let mut bad = policy(SessionId::new(), Ipv4Addr::new(10, 200, 0, 10)); @@ -424,7 +464,8 @@ mod tests { let reply = sync_policies( &state, vec![bad, policy(good, Ipv4Addr::new(10, 200, 0, 14))], - ); + ) + .await; assert_eq!(reply, FromProxyd::Ok); assert_eq!(state.registry.live_count(), 1); assert!(state @@ -438,17 +479,19 @@ mod tests { /// a total egress lockout by the sync's replace half. The failed /// update leaves the existing registration live under its previous /// policy (the ADR 0111 "reduce, never strand" posture). - #[test] - fn sync_keeps_a_live_session_whose_updated_policy_is_untranslatable() { + #[tokio::test] + async fn sync_keeps_a_live_session_whose_updated_policy_is_untranslatable() { let state = state(); let session = SessionId::new(); let ip = Ipv4Addr::new(10, 200, 0, 18); - register_policy(&state.registry, policy(session, ip)).expect("initial apply"); + register_policy(&state.registry, policy(session, ip)) + .await + .expect("initial apply"); assert!(state.registry.lookup(ip).is_some()); let mut update = policy(session, ip); update.network_allow_host_patterns = vec!["[".into()]; // untranslatable - let reply = sync_policies(&state, vec![update]); + let reply = sync_policies(&state, vec![update]).await; assert_eq!(reply, FromProxyd::Ok); let survivor = state .registry @@ -466,26 +509,30 @@ mod tests { /// posture-mapping tests live) with a scoped allowlist must /// translate + register cleanly. Moved from `pooled_backend.rs` /// with the translation itself (ADR 0121). - #[test] - fn capture_shaped_allowlist_policy_registers() { + #[tokio::test] + async fn capture_shaped_allowlist_policy_registers() { let mut p = policy(SessionId::new(), Ipv4Addr::new(169, 254, 0, 2)); p.network_allow_hosts = vec!["accounts.google.com".into()]; p.network_allow_host_patterns = vec!["*.auth0.com".into()]; let registry = Registry::new(); - register_policy(®istry, p).expect("proxy must accept the capture-egress allowlist"); + register_policy(®istry, p) + .await + .expect("proxy must accept the capture-egress allowlist"); } /// A capture-shaped allow-all policy must register, and the proxy /// must bypass an arbitrary host under it (the dev posture for an /// image whose warm boot needs unrestricted network). - #[test] - fn capture_shaped_allow_all_policy_bypasses() { + #[tokio::test] + async fn capture_shaped_allow_all_policy_bypasses() { let mut p = policy(SessionId::new(), Ipv4Addr::new(169, 254, 0, 3)); p.network_allow_hosts = Vec::new(); p.allow_all = true; let guest_ip = p.guest_ip; let registry = Registry::new(); - register_policy(®istry, p).expect("register allow-all"); + register_policy(®istry, p) + .await + .expect("register allow-all"); let state = registry.lookup(guest_ip).expect("registered"); assert!(matches!( state.decide("anything.example.com"), @@ -493,8 +540,8 @@ mod tests { )); } - #[test] - fn sha256_hex_is_stable() { + #[tokio::test] + async fn sha256_hex_is_stable() { assert_eq!( sha256_hex(b"abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" diff --git a/crates/engram-egress-proxyd/src/lib.rs b/crates/engram-egress-proxyd/src/lib.rs index 0a281d49f..82cc09f33 100644 --- a/crates/engram-egress-proxyd/src/lib.rs +++ b/crates/engram-egress-proxyd/src/lib.rs @@ -193,6 +193,7 @@ pub async fn run(args: DaemonArgs) -> i32 { // durable-at-the-point-it's-known rule, PR #1383 / ADR 0121). let identity = engram_egress_proto::manifest::self_identity(); let hello = engram_egress_proto::HelloInfo { + incarnation: engram_core::SessionId::new().to_string(), proto_version: engram_egress_proto::PROTO_VERSION, source_fingerprint: SOURCE_FINGERPRINT.map(str::to_string), proxy_port: args.proxy_port, diff --git a/crates/engram-egress-proxyd/tests/spawn_serve.rs b/crates/engram-egress-proxyd/tests/spawn_serve.rs index 198c76888..0be967ade 100644 --- a/crates/engram-egress-proxyd/tests/spawn_serve.rs +++ b/crates/engram-egress-proxyd/tests/spawn_serve.rs @@ -224,6 +224,7 @@ fn loopback_policy( session_id: engram_core::SessionId, ) -> engram_core::types::egress::SessionEgressPolicy { engram_core::types::egress::SessionEgressPolicy { + managed_deny: None, session_id, sandbox_id: engram_core::SandboxId::new(), guest_ip: std::net::Ipv4Addr::LOCALHOST, diff --git a/crates/engram-host-agent/src/bindings.rs b/crates/engram-host-agent/src/bindings.rs index a85b55c3e..45c51cea3 100644 --- a/crates/engram-host-agent/src/bindings.rs +++ b/crates/engram-host-agent/src/bindings.rs @@ -106,6 +106,21 @@ impl BindingStore { }) } + pub(crate) fn managed_required(&self, session: SessionId) -> bool { + self.dir + .join(format!("{session}.managed-required")) + .try_exists() + .unwrap_or(true) + || self + .dir + .join(format!("{session}.managed")) + .try_exists() + .unwrap_or(true) + } + pub(crate) fn directory(&self) -> &std::path::Path { + &self.dir + } + fn path_for(&self, session_id: SessionId) -> PathBuf { self.dir.join(format!("{session_id}.json")) } @@ -302,6 +317,23 @@ impl BindingStore { } } } + for entry in std::fs::read_dir(&self.dir)? { + let entry = entry?; + if entry.path().extension().and_then(|e| e.to_str()) != Some("managed-required") { + continue; + } + let sid: SessionId = entry + .path() + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| std::io::Error::other("invalid managed filename"))? + .parse() + .map_err(std::io::Error::other)?; + let record = crate::managed_policy::read_record(self, sid)? + .ok_or_else(|| std::io::Error::other("managed record missing"))?; + out.retain(|p| p.session_id != sid); + out.push(record.egress); + } Ok(out) } @@ -402,6 +434,7 @@ mod tests { sandbox_id: SandboxId, ) -> engram_core::types::egress::SessionEgressPolicy { engram_core::types::egress::SessionEgressPolicy { + managed_deny: None, session_id, sandbox_id, guest_ip: std::net::Ipv4Addr::new(10, 200, 0, 2), diff --git a/crates/engram-host-agent/src/config.rs b/crates/engram-host-agent/src/config.rs index 3c58ee986..a9056e840 100644 --- a/crates/engram-host-agent/src/config.rs +++ b/crates/engram-host-agent/src/config.rs @@ -3,6 +3,7 @@ use std::time::Duration; #[derive(Clone, Debug)] pub struct HostAgentConfig { + pub managed_tls: Option, /// Where the SandboxBackend stores per-VM working directories, /// Firecracker sockets, and snapshot files. pub work_dir: PathBuf, @@ -55,6 +56,7 @@ pub struct HostAgentConfig { impl Default for HostAgentConfig { fn default() -> Self { Self { + managed_tls: None, work_dir: PathBuf::from("./var/sandboxes"), heartbeat_interval: Duration::from_secs(5), local_snapshot_cap_bytes: 500 * 1024 * 1024 * 1024, // 500 GiB diff --git a/crates/engram-host-agent/src/grpc_server.rs b/crates/engram-host-agent/src/grpc_server.rs index 2cfc889dd..68e42dd81 100644 --- a/crates/engram-host-agent/src/grpc_server.rs +++ b/crates/engram-host-agent/src/grpc_server.rs @@ -69,6 +69,7 @@ fn link_remote_parent(span: &tracing::Span, req: &Request) { /// `HostAdminHandler` services ReapMaterializeDir. pub struct HostServiceImpl { inner: Arc, + managed_tls: bool, admin: Option>, /// ADR 0079: per-session fencing-epoch high-water, durable under /// work_dir. Gates every session-scoped lifecycle RPC. @@ -84,6 +85,7 @@ impl HostServiceImpl { pub fn new(inner: Arc, epochs: SessionEpochStore) -> Self { Self { inner, + managed_tls: false, admin: None, epochs, peer: None, @@ -174,6 +176,39 @@ pub async fn boot( admin: Option>, epochs: SessionEpochStore, peer: Option>, +) -> Result<(), tonic::transport::Error> { + serve(listen_addr, inner, admin, epochs, peer, None).await +} + +/// Serve one authenticated listener for all lifecycle operations in managed mode. +pub async fn boot_with_managed_tls( + listen_addr: std::net::SocketAddr, + inner: Arc, + admin: Option>, + epochs: SessionEpochStore, + peer: Option>, + tls: Option, +) -> Result<(), Box> { + let tls = tls + .map(|config| { + let server = config.server_config().map_err(std::io::Error::other)?; + Ok::<_, std::io::Error>((server, config.coordinator_pins)) + }) + .transpose()?; + serve(listen_addr, inner, admin, epochs, peer, tls).await?; + Ok(()) +} + +async fn serve( + listen_addr: std::net::SocketAddr, + inner: Arc, + admin: Option>, + epochs: SessionEpochStore, + peer: Option>, + tls: Option<( + tonic::transport::ServerTlsConfig, + std::collections::BTreeSet, + )>, ) -> Result<(), tonic::transport::Error> { let mut svc = HostServiceImpl::new(inner, epochs); if let Some(a) = admin { @@ -182,8 +217,42 @@ pub async fn boot( if let Some(p) = peer { svc = svc.with_peer_serve(p); } + svc.managed_tls = tls.is_some(); + let (server, pins) = match tls { + Some((config, pins)) => ( + tonic::transport::Server::builder().tls_config(config)?, + Some(pins), + ), + None => (tonic::transport::Server::builder(), None), + }; + let service = HostServiceServer::new(svc).max_encoding_message_size(32 * 1024 * 1024); + let authenticated = + tonic::service::interceptor::InterceptedService::new(service, move |req: Request<()>| { + if let Some(pins) = &pins { + let certificates = req + .peer_certs() + .ok_or_else(|| Status::unauthenticated("client certificate required"))?; + let leaf = certificates + .first() + .ok_or_else(|| Status::unauthenticated("client certificate required"))?; + if !pins.contains(&engram_protocol::host_tls::certificate_pin(leaf.as_ref())) { + return Err(Status::unauthenticated("client certificate not authorized")); + } + let version = req + .metadata() + .get(engram_protocol::wire::WIRE_VERSION_METADATA_KEY) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse::().ok()); + if version != Some(engram_protocol::WIRE_VERSION) { + return Err(Status::failed_precondition( + "host protocol version required", + )); + } + } + Ok(req) + }); tracing::info!(addr = %listen_addr, "gRPC HostService listening"); - tonic::transport::Server::builder() + server // ADR 0013: HTTP/2 multiplexes many concurrent streams over // one TCP connection per coord pod. 256 is well above the // running_sandboxes ceiling — we don't expect to hit it. @@ -197,7 +266,7 @@ pub async fn boot( .initial_stream_window_size(Some(16 * 1024 * 1024)) .initial_connection_window_size(Some(32 * 1024 * 1024)) .http2_adaptive_window(Some(true)) - .add_service(HostServiceServer::new(svc).max_encoding_message_size(32 * 1024 * 1024)) + .add_service(authenticated) .serve(listen_addr) .await } @@ -997,6 +1066,60 @@ impl HostService for HostServiceImpl { /// warm-lease path doesn't reach here — it goes through /// `WarmPool::launch` (which has its own histogram emission /// labelled `kind="warm"`). + async fn read_managed_policy( + &self, + req: Request, + ) -> Result, Status> { + check_wire_version(&req)?; + if !self.managed_tls { + return Err(Status::failed_precondition( + "managed policy requires authenticated host mode", + )); + } + if req.get_ref().target_bincode.len() > 4096 { + return Err(Status::resource_exhausted("managed request too large")); + } + let target = engram_protocol::managed_policy::decode(&req.into_inner().target_bincode) + .map_err(|_| Status::invalid_argument("invalid managed target"))?; + let state = self + .inner + .read_managed_policy(target) + .await + .map_err(sandbox_to_status)?; + Ok(Response::new( + engram_protocol::grpc::ReadManagedPolicyResponse { + state_bincode: bincode::serialize(&state) + .map_err(|_| Status::internal("managed response encoding failed"))?, + }, + )) + } + async fn apply_managed_policy( + &self, + req: Request, + ) -> Result, Status> { + check_wire_version(&req)?; + if !self.managed_tls { + return Err(Status::failed_precondition( + "managed policy requires authenticated host mode", + )); + } + if req.get_ref().request_bincode.len() > 4 * 1024 * 1024 { + return Err(Status::resource_exhausted("managed request too large")); + } + let request = engram_protocol::managed_policy::decode(&req.into_inner().request_bincode) + .map_err(|_| Status::invalid_argument("invalid managed request"))?; + let state = self + .inner + .apply_managed_policy(request) + .await + .map_err(sandbox_to_status)?; + Ok(Response::new( + engram_protocol::grpc::ApplyManagedPolicyResponse { + state_bincode: bincode::serialize(&state) + .map_err(|_| Status::internal("managed response encoding failed"))?, + }, + )) + } async fn start_agent( &self, req: Request, diff --git a/crates/engram-host-agent/src/harness.rs b/crates/engram-host-agent/src/harness.rs index 2065d3218..17d3c84f2 100644 --- a/crates/engram-host-agent/src/harness.rs +++ b/crates/engram-host-agent/src/harness.rs @@ -213,6 +213,10 @@ impl HarnessHub { /// record, so a restarted host-agent rebuilds its proxy registry /// from a local read. Called on the `start_agent` path AFTER the /// in-memory registration succeeds and BEFORE the ack returns. + pub(crate) fn binding_store(&self) -> crate::bindings::BindingStore { + self.inner.bindings.clone() + } + pub fn persist_egress_policy( &self, policy: &engram_core::types::egress::SessionEgressPolicy, diff --git a/crates/engram-host-agent/src/host_client.rs b/crates/engram-host-agent/src/host_client.rs index 0a274c89e..910b28f85 100644 --- a/crates/engram-host-agent/src/host_client.rs +++ b/crates/engram-host-agent/src/host_client.rs @@ -11,7 +11,7 @@ //! `engram-protocol::server` serves over the WS — so the coord on //! the other end of a `RemoteHostClient` is talking to one of these. //! -//! All sandbox methods are pure delegation to the inner `SandboxBackend`. +//! Sandbox methods delegate after the applicable host lifecycle guards. //! The harness methods (`bind_session`, `unbind_session`, //! `send_prompt`) delegate to the `HarnessHub`, mapping its //! `HarnessError` into `SandboxError` so the trait surface stays @@ -38,6 +38,7 @@ use crate::harness::{HarnessError, HarnessHub}; pub struct LocalHostClient { sandbox: Arc, harness_hub: Arc, + managed: Option>, } impl LocalHostClient { @@ -45,9 +46,46 @@ impl LocalHostClient { Self { sandbox, harness_hub, + managed: None, } } + pub fn with_managed_policy( + mut self, + host: Arc, + ) -> Self { + self.managed = Some(host); + self + } + + fn check_restore_gate( + &self, + fence: SessionFence, + sandbox: Option, + ) -> Result<(), SandboxError> { + let bindings = self.harness_hub.binding_store(); + let mut managed = bindings.managed_required(fence.session_id); + if let Some(sandbox) = sandbox { + if let Some(binding) = bindings + .find_by_sandbox(sandbox) + .map_err(|e| SandboxError::InvalidSpec(e.to_string()))? + { + managed |= bindings.managed_required(binding.session_id); + } + } + if managed { + return Err(SandboxError::InvalidSpec( + "managed restore quarantine unavailable".into(), + )); + } + if self.managed.is_some() && fence.session_id.as_uuid().is_nil() { + return Err(SandboxError::InvalidSpec( + "restore session identity required".into(), + )); + } + Ok(()) + } + /// Convenience constructor for callers that don't need to route /// harness events through a shared hub (tests, in-process glue /// where the harness path isn't exercised). Builds a `HarnessHub` @@ -254,8 +292,9 @@ impl HostClient for LocalHostClient { async fn restore( &self, metadata: SnapshotMetadata, - _fence: SessionFence, + fence: SessionFence, ) -> Result { + self.check_restore_gate(fence, metadata.source_sandbox_id)?; self.sandbox.restore(metadata).await } @@ -285,13 +324,34 @@ impl HostClient for LocalHostClient { metadata: SnapshotMetadata, session_env: std::collections::HashMap, selected_mounts: Vec, - _fence: SessionFence, + fence: SessionFence, ) -> Result { + self.check_restore_gate(fence, metadata.source_sandbox_id)?; self.sandbox .restore_base_for_session(metadata, session_env, selected_mounts) .await } + async fn read_managed_policy( + &self, + target: engram_core::types::managed_deny::ManagedPolicyTarget, + ) -> Result, SandboxError> { + self.managed + .as_ref() + .ok_or_else(|| SandboxError::InvalidSpec("managed host mode unavailable".into()))? + .read_applied(&target) + .await + } + async fn apply_managed_policy( + &self, + request: engram_core::types::managed_deny::ManagedPolicyApply, + ) -> Result { + self.managed + .as_ref() + .ok_or_else(|| SandboxError::InvalidSpec("managed host mode unavailable".into()))? + .compare_and_apply(request) + .await + } async fn start_agent( &self, id: SandboxId, @@ -299,6 +359,16 @@ impl HostClient for LocalHostClient { policy: SessionEgressPolicy, _fence: SessionFence, ) -> Result<(), SandboxError> { + if policy.managed_deny.is_some() + || self + .harness_hub + .binding_store() + .managed_required(policy.session_id) + { + return Err(SandboxError::InvalidSpec( + "managed session requires acknowledged boot finalization".into(), + )); + } // ADR 0073: persist the binding record BEFORE the harness can // dial (the spawn below), so the first attach validates instead // of bouncing UnknownBinding. Monotonic: a stale caller's bind @@ -420,11 +490,8 @@ impl HostClient for LocalHostClient { self.sandbox.pause(sandbox_id).await } - async fn resume( - &self, - sandbox_id: SandboxId, - _fence: SessionFence, - ) -> Result<(), SandboxError> { + async fn resume(&self, sandbox_id: SandboxId, fence: SessionFence) -> Result<(), SandboxError> { + self.check_restore_gate(fence, Some(sandbox_id))?; self.sandbox.resume(sandbox_id).await } diff --git a/crates/engram-host-agent/src/lib.rs b/crates/engram-host-agent/src/lib.rs index 2cd04371c..728bfe32b 100644 --- a/crates/engram-host-agent/src/lib.rs +++ b/crates/engram-host-agent/src/lib.rs @@ -74,6 +74,7 @@ pub use config::HostAgentConfig; pub struct HostAgent { pub cfg: HostAgentConfig, + managed_ownership: Option>, pub sandbox: Arc, pub cloud: Arc, /// ADR 0006: local egress proxy. The production host-agent binary @@ -131,6 +132,7 @@ impl HostAgent { ) -> Self { Self { cfg, + managed_ownership: None, sandbox, cloud, egress: None, @@ -226,6 +228,14 @@ impl HostAgent { self } + pub fn with_managed_ownership( + mut self, + ownership: Option>, + ) -> Self { + self.managed_ownership = ownership; + self + } + /// Run the host agent's background loops until shutdown. /// /// Phase 3a: if `coordinator_endpoint` is set, runs the WS dialer @@ -234,6 +244,27 @@ impl HostAgent { /// in standalone dev where the binary just hosts a local backend /// without phoning home. pub async fn run(self) -> Result<(), HostAgentError> { + let managed_ownership = match self.managed_ownership.clone() { + Some(owner) => Some(owner), + None => managed_policy::ManagedOwnership::for_mode( + &self.cfg.work_dir.join("bindings"), + self.cfg.managed_tls.is_some(), + )?, + }; + if let Some(tls) = &self.cfg.managed_tls { + if self.cfg.grpc_listen_addr.is_none() + || !self + .cfg + .grpc_advertise_addr + .as_ref() + .is_some_and(|a| a.starts_with("https://")) + || self.egress.is_none() + || self.host_id.is_none() + { + return Err(HostAgentError::Config("managed host mode requires an HTTPS lifecycle listener, stable host identity, and egress proxy".into())); + } + tls.server_config().map_err(HostAgentError::Config)?; + } tracing::info!(?self.cfg.work_dir, "host-agent starting"); let _ = self.cloud.host_metadata().await; @@ -771,6 +802,29 @@ impl HostAgent { .expect("open binding store under work_dir (ADR 0073)"); let harness_hub = std::sync::Arc::new(crate::harness::HarnessHub::new(event_sink, bindings)); + let managed_policy = if self.cfg.managed_tls.is_some() { + use engram_core::traits::Entropy; + let managed = crate::managed_policy::ManagedPolicyHost::new( + host_id, + engram_core::traits::OsEntropy.uuid().to_string(), + harness_hub.binding_store(), + pooled.clone(), + self.egress.clone().expect("managed configuration checked"), + managed_ownership + .clone() + .expect("managed ownership acquired"), + )?; + Some(Arc::new(managed)) + } else { + None + }; + if let Some(managed) = &managed_policy { + managed + .recover_all() + .await + .map_err(|e| HostAgentError::Config(e.to_string()))?; + } + // Plumb the hub into the FC/VZ backend's vsock-accept // sink so inbound harness connections land on the local // hub's adapter loop. Without this the FC backend drops @@ -792,6 +846,9 @@ impl HostAgent { match harness_hub.persisted_egress_policies() { Ok(policies) => pooled.rebuild_egress_from_policies(policies).await, Err(e) => { + if self.cfg.managed_tls.is_some() { + return Err(HostAgentError::Io(e)); + } tracing::warn!(error = %e, "list persisted egress policies failed"); } } @@ -811,11 +868,19 @@ impl HostAgent { // persisted policies. Not armed until here because the // replay needs the pooled backend. let hub_for_replay = harness_hub.clone(); + let managed_for_replay = managed_policy.clone(); let pooled_for_replay = pooled.clone(); egress.spawn_supervisor(std::sync::Arc::new(move || { let hub = hub_for_replay.clone(); + let managed = managed_for_replay.clone(); let pooled = pooled_for_replay.clone(); Box::pin(async move { + if let Some(managed) = managed { + if let Err(error) = managed.recover_all().await { + tracing::error!(%error, "managed proxy recovery failed; admission remains closed"); + return; + } + } match hub.persisted_egress_policies() { Ok(policies) => pooled.rebuild_egress_from_policies(policies).await, Err(e) => { @@ -915,9 +980,12 @@ impl HostAgent { pooled.set_upload_sink(upload_sink); // ADR 0015 M5: warm-pool retired. LocalHostClient is just // the cold-create dispatch wrapper now. - let local_host: Arc = Arc::new( - crate::host_client::LocalHostClient::new(pooled.clone(), harness_hub.clone()), - ); + let mut local = + crate::host_client::LocalHostClient::new(pooled.clone(), harness_hub.clone()); + if let Some(managed) = managed_policy { + local = local.with_managed_policy(managed); + } + let local_host: Arc = Arc::new(local); // ADR 0009 §2: populate `running_sandboxes` from // `backend.list()` on each heartbeat tick. The coord // intersects this against expected-active sessions to @@ -1243,6 +1311,7 @@ impl HostAgent { // `grpc_listen_addr=None`. let grpc_task = self.cfg.grpc_listen_addr.map(|addr| { let local_for_grpc = local_host.clone(); + let managed_tls = self.cfg.managed_tls.clone(); let admin_for_grpc = admin_handler.clone(); let peer_for_grpc = peer_serve.clone(); // ADR 0079: the per-session fencing-epoch high-water, @@ -1252,12 +1321,13 @@ impl HostAgent { ) .expect("open session epoch store under work_dir (ADR 0079)"); tokio::spawn(async move { - if let Err(e) = grpc_server::boot( + if let Err(e) = grpc_server::boot_with_managed_tls( addr, local_for_grpc, admin_for_grpc, epochs, peer_for_grpc, + managed_tls, ) .await { @@ -2440,3 +2510,7 @@ mod tests { assert!(adopt_handoff_marker(dir.path()).is_none()); } } + +pub mod managed_policy; + +pub mod managed_tls; diff --git a/crates/engram-host-agent/src/main.rs b/crates/engram-host-agent/src/main.rs index 81d3679cf..e8387149f 100644 --- a/crates/engram-host-agent/src/main.rs +++ b/crates/engram-host-agent/src/main.rs @@ -311,6 +311,8 @@ async fn main() -> Result<(), HostAgentError> { let (migrate_peer_listen_addr, _) = parse_grpc_listen(&cli.migrate_peer_listen_addr); let cfg = HostAgentConfig { + managed_tls: engram_host_agent::managed_tls::ManagedHostTls::from_env() + .map_err(engram_host_agent::HostAgentError::Config)?, work_dir: cli.work_dir.clone(), coordinator_endpoint: cli.coordinator.clone(), coordinator_token: cli.coordinator_token.clone(), @@ -327,6 +329,11 @@ async fn main() -> Result<(), HostAgentError> { ..HostAgentConfig::default() }; + let managed_ownership = engram_host_agent::managed_policy::ManagedOwnership::for_mode( + &cli.work_dir.join("bindings"), + cfg.managed_tls.is_some(), + )?; + // ADR 0007 Phase 5: resolve the HostId early so the FC config can // stamp it on snapshots' `trace_host_hint` and pass it as // `--publish-trace-host` to the UFFD handler. Cross-host trace replay @@ -651,6 +658,7 @@ async fn main() -> Result<(), HostAgentError> { let observe_token = cfg.coordinator_token.clone(); let mut agent = HostAgent::new(cfg, sandbox, cloud) + .with_managed_ownership(managed_ownership) .with_chunk_store(chunk_store, materialize_dir) .with_chunk_cache(chunk_cache) .with_dirty_root(cli.dirty_root.clone()) diff --git a/crates/engram-host-agent/src/managed_policy.rs b/crates/engram-host-agent/src/managed_policy.rs new file mode 100644 index 000000000..27a6bf5de --- /dev/null +++ b/crates/engram-host-agent/src/managed_policy.rs @@ -0,0 +1,881 @@ +//! Durable managed policy application. It does not start a guest. + +use crate::{bindings::BindingStore, egress::HostEgress}; +use engram_core::{ + traits::SandboxBackend, types::managed_deny::*, HostId, SandboxError, SessionId, +}; +use serde::{Deserialize, Serialize}; +use std::{io::Write, sync::Arc}; + +#[derive(Clone, Serialize, Deserialize)] +pub(crate) struct ManagedRecord { + version: u32, + context: ManagedPolicyContext, + pub(crate) egress: engram_core::types::egress::SessionEgressPolicy, + applied: Option, + expected: Option, + accepted_host_incarnation: String, + accepted_proxy_incarnation: String, +} +fn invalid(e: impl std::fmt::Display) -> SandboxError { + SandboxError::InvalidSpec(e.to_string()) +} +fn io_invalid(e: impl std::fmt::Display) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()) +} +fn write_atomic( + store: &BindingStore, + sid: SessionId, + suffix: &str, + bytes: &[u8], +) -> std::io::Result<()> { + let path = store.directory().join(format!("{sid}.{suffix}")); + let temp = path.with_extension(format!("{suffix}.partial")); + let mut opts = std::fs::OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); + } + let mut file = opts.open(&temp)?; + file.write_all(bytes)?; + file.sync_all()?; + std::fs::rename(temp, path)?; + std::fs::File::open(store.directory())?.sync_all() +} +fn persist(store: &BindingStore, record: &ManagedRecord) -> std::io::Result<()> { + let sid = record.context.target.session_id; + // This completeness marker makes a missing record distinguishable from no policy. + write_atomic(store, sid, "managed-required", b"1")?; + let body = serde_json::to_string(record).map_err(io_invalid)?; + write_atomic( + store, + sid, + "managed", + &crate::durable_envelope::seal(&sid.to_string(), &body), + ) +} +pub(crate) fn read_record( + store: &BindingStore, + sid: SessionId, +) -> std::io::Result> { + let path = store.directory().join(format!("{sid}.managed")); + let bytes = match std::fs::read(path) { + Ok(v) => v, + Err(e) + if e.kind() == std::io::ErrorKind::NotFound + && !store + .directory() + .join(format!("{sid}.managed-required")) + .exists() => + { + return Ok(None) + } + Err(e) => return Err(e), + }; + if std::fs::read(store.directory().join(format!("{sid}.managed-required")))? != b"1" { + return Err(io_invalid("invalid managed completeness marker")); + } + let body = crate::durable_envelope::open(&bytes, &sid.to_string()).map_err(io_invalid)?; + let record: ManagedRecord = serde_json::from_slice(&body).map_err(io_invalid)?; + record.context.validate().map_err(io_invalid)?; + if record.version != 1 + || record.context.target.session_id != sid + || record.egress.session_id != sid + || record.egress.sandbox_id != record.context.target.sandbox_id + { + return Err(io_invalid("invalid managed record identity")); + } + record + .egress + .managed_deny + .as_ref() + .ok_or_else(|| io_invalid("missing managed deny"))? + .validate() + .map_err(io_invalid)?; + if let Some(applied) = &record.applied { + applied.validate().map_err(io_invalid)?; + if applied.context != record.context + || Some(&applied.policy) != record.egress.managed_deny.as_ref() + { + return Err(io_invalid( + "managed applied marker differs from desired policy", + )); + } + } + Ok(Some(record)) +} + +/// Exclusive ownership of this directory. The stable lock file is also the +/// durable marker that prevents a later plaintext host from reopening it. +pub struct ManagedOwnership { + _file: std::fs::File, + directory: std::path::PathBuf, +} +impl ManagedOwnership { + pub fn for_mode( + directory: &std::path::Path, + managed: bool, + ) -> std::io::Result>> { + let path = directory.join("managed-owner.lock"); + if !managed { + if path.exists() { + return Err(io_invalid( + "managed host directory requires authenticated mode", + )); + } + return Ok(None); + } + std::fs::create_dir_all(directory)?; + let mut options = std::fs::OpenOptions::new(); + options.read(true).write(true).create(true).truncate(false); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600).custom_flags(libc::O_CLOEXEC); + } + let file = options.open(path)?; + file.try_lock().map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::WouldBlock, + "managed host directory is already owned", + ) + })?; + file.sync_all()?; + std::fs::File::open(directory)?.sync_all()?; + Ok(Some(Arc::new(Self { + _file: file, + directory: directory.canonicalize()?, + }))) + } +} + +pub struct ManagedPolicyHost { + _ownership: Arc, + host_id: HostId, + incarnation: String, + bindings: BindingStore, + backend: Arc, + egress: Arc, + locks: dashmap::DashMap>>, +} +impl ManagedPolicyHost { + pub fn new( + host_id: HostId, + incarnation: String, + bindings: BindingStore, + backend: Arc, + egress: Arc, + ownership: Arc, + ) -> std::io::Result { + if ownership.directory != bindings.directory().canonicalize()? { + return Err(io_invalid("managed ownership directory mismatch")); + } + Ok(Self { + _ownership: ownership, + host_id, + incarnation, + bindings, + backend, + egress, + locks: dashmap::DashMap::new(), + }) + } + /// Quarantine trusted proxy registrations before reading any durable policy. + /// Ordinary replay must run only after this succeeds. + pub async fn recover_all(&self) -> Result<(), SandboxError> { + let handle = self.egress.handle(); + let known: std::collections::BTreeSet<_> = handle + .managed_sessions() + .await + .map_err(invalid)? + .into_iter() + .collect(); + for sid in &known { + handle.quarantine_managed(*sid).await.map_err(invalid)?; + } + let mut sessions = known.clone(); + for entry in std::fs::read_dir(self.bindings.directory()).map_err(invalid)? { + let path = entry.map_err(invalid)?.path(); + if matches!( + path.extension().and_then(|s| s.to_str()), + Some("managed" | "managed-required") + ) { + sessions.insert( + path.file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| invalid("invalid managed filename"))? + .parse::() + .map_err(invalid)?, + ); + } + } + for sid in &sessions { + handle.quarantine_managed(*sid).await.map_err(invalid)?; + } + // Validate the complete set before reopening any session. + let mut records = Vec::new(); + for sid in &sessions { + let sid = *sid; + let record = read_record(&self.bindings, sid) + .map_err(invalid)? + .ok_or_else(|| invalid("managed record missing"))?; + if !known.contains(&sid) && self.bindings.read(sid).map_err(invalid)?.is_none() { + continue; + } + self.check_record(&record).await?; + records.push(record); + } + for record in records { + if let Err(error) = self.read_applied(&record.context.target).await { + for sid in &sessions { + handle.quarantine_managed(*sid).await.map_err(invalid)?; + } + return Err(error); + } + } + Ok(()) + } + async fn check_target(&self, target: &ManagedPolicyTarget) -> Result<(), SandboxError> { + if target.host_id != self.host_id { + return Err(invalid("managed host identity mismatch")); + } + let binding = self + .bindings + .read(target.session_id) + .map_err(invalid)? + .ok_or_else(|| invalid("managed session binding absent"))?; + if binding.sandbox_id != target.sandbox_id || binding.binding_epoch != target.binding_epoch + { + return Err(invalid("managed physical binding changed")); + } + let endpoint = self + .backend + .guest_endpoints(target.sandbox_id) + .await + .ok_or_else(|| invalid("managed guest identity absent"))?; + if endpoint.egress_identity.is_unspecified() { + return Err(invalid("managed guest identity absent")); + } + Ok(()) + } + async fn check_record( + &self, + record: &ManagedRecord, + ) -> Result, SandboxError> { + self.check_target(&record.context.target).await?; + let endpoint = self + .backend + .guest_endpoints(record.context.target.sandbox_id) + .await + .ok_or_else(|| invalid("managed guest identity absent"))?; + if endpoint.egress_identity != record.egress.guest_ip { + return Err(invalid("managed guest identity mismatch")); + } + if !record.egress.tunnels.is_empty() { + return Err(invalid("managed policy does not support tunnels")); + } + let proof = self + .backend + .managed_network_proof(&record.context.target) + .await; + if record.context.binding_hash.is_some() && proof.is_none() { + return Err(invalid("strict managed network capability unavailable")); + } + if proof.as_ref().is_some_and(|p| { + p.target != record.context.target + || p.guest_ip != record.egress.guest_ip + || !is_digest(&p.network_identity) + }) { + return Err(invalid("strict managed network identity mismatch")); + } + Ok(proof) + } + async fn replay(&self, mut record: ManagedRecord) -> Result { + let proof = self.check_record(&record).await?; + let sid = record.context.target.session_id; + let proxy = self + .egress + .handle() + .read_managed(sid) + .await + .map_err(invalid)?; + let matches = proxy.as_ref().is_some_and(|p| { + p.sandbox_id == record.egress.sandbox_id + && p.guest_ip == record.egress.guest_ip + && Some(&p.policy) == record.egress.managed_deny.as_ref() + }); + if let Some(applied) = &record.applied { + if matches + && applied.network_proof == proof + && applied.host_incarnation == self.incarnation + && proxy + .as_ref() + .is_some_and(|p| p.incarnation == applied.proxy_incarnation) + { + return Ok(applied.clone()); + } + } + self.backend + .notify_session_policy(record.egress.clone()) + .await?; + let actual = self + .egress + .handle() + .read_managed(sid) + .await + .map_err(invalid)? + .ok_or_else(|| invalid("managed policy not installed"))?; + if actual.sandbox_id != record.egress.sandbox_id + || actual.guest_ip != record.egress.guest_ip + || Some(&actual.policy) != record.egress.managed_deny.as_ref() + { + return Err(invalid("managed proxy acknowledgement mismatch")); + } + self.check_target(&record.context.target).await?; + let applied = ManagedAppliedState::new( + record.context.clone(), + actual.policy, + self.incarnation.clone(), + actual.incarnation, + proof, + ) + .map_err(invalid)?; + record.applied = Some(applied.clone()); + persist(&self.bindings, &record).map_err(invalid)?; + Ok(applied) + } + async fn read_inner( + &self, + target: &ManagedPolicyTarget, + ) -> Result, SandboxError> { + self.check_target(target).await?; + match read_record(&self.bindings, target.session_id).map_err(invalid)? { + Some(record) => { + if &record.context.target != target { + return Err(invalid("managed policy belongs to another placement")); + } + self.replay(record).await.map(Some) + } + None => Ok(None), + } + } + pub async fn read_applied( + &self, + target: &ManagedPolicyTarget, + ) -> Result, SandboxError> { + let lock = self.locks.entry(target.session_id).or_default().clone(); + let _guard = lock.lock().await; + let result = self.read_inner(target).await; + if result.is_err() { + let _ = self + .egress + .handle() + .quarantine_managed(target.session_id) + .await; + } + result + } + pub async fn compare_and_apply( + &self, + request: ManagedPolicyApply, + ) -> Result { + request.context.validate().map_err(invalid)?; + if !request.egress.tunnels.is_empty() { + return Err(invalid("managed policy does not support tunnels")); + } + let target = request.context.target.clone(); + let lock = self.locks.entry(target.session_id).or_default().clone(); + let _guard = lock.lock().await; + self.check_target(&target).await?; + let policy = request + .egress + .managed_deny + .as_ref() + .ok_or_else(|| invalid("managed policy missing"))?; + policy.validate().map_err(invalid)?; + let endpoint = self + .backend + .guest_endpoints(target.sandbox_id) + .await + .ok_or_else(|| invalid("managed guest identity absent"))?; + if request.egress.session_id != target.session_id + || request.egress.sandbox_id != target.sandbox_id + || request.egress.guest_ip != endpoint.egress_identity + { + return Err(invalid("managed guest identity mismatch")); + } + if let Some(expected) = &request.expected { + expected.validate().map_err(invalid)?; + } + let current = match self.read_inner(&target).await { + Ok(value) => value, + Err(error) => { + let _ = self + .egress + .handle() + .quarantine_managed(target.session_id) + .await; + return Err(error); + } + }; + if let Some(state) = ¤t { + // An exact lost-response retry has already committed this candidate. + if state.context == request.context && &state.policy == policy { + let record = read_record(&self.bindings, target.session_id) + .map_err(invalid)? + .ok_or_else(|| invalid("managed record absent"))?; + if request.expected == current + || (request.expected == record.expected + && state.host_incarnation == record.accepted_host_incarnation + && state.proxy_incarnation == record.accepted_proxy_incarnation) + { + return Ok(state.clone()); + } + return Err(invalid("managed expected state changed")); + } + } + if current != request.expected { + return Err(invalid("managed expected state changed")); + } + if let Some(old) = ¤t { + if policy.revision <= old.policy.revision + || !policy.contains(&old.policy) + || (request.context.operator_generation < old.context.operator_generation + || (request.context.operator_generation == old.context.operator_generation + && request.context.operator_identity != old.context.operator_identity)) + { + return Err(invalid("managed policy regressed")); + } + } + let proxy_incarnation = self + .egress + .handle() + .hello() + .await + .map_err(invalid)? + .incarnation; + let record = ManagedRecord { + accepted_host_incarnation: self.incarnation.clone(), + accepted_proxy_incarnation: proxy_incarnation, + version: 1, + context: request.context, + egress: request.egress, + applied: None, + expected: request.expected, + }; + persist(&self.bindings, &record).map_err(invalid)?; + match self.replay(record).await { + Ok(state) => Ok(state), + Err(error) => { + let _ = self + .egress + .handle() + .quarantine_managed(target.session_id) + .await; + Err(error) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn torn_managed_record_never_becomes_absent() { + let dir = tempfile::tempdir().unwrap(); + let sid = engram_core::SessionId::new(); + let store = crate::bindings::BindingStore::open(dir.path()).unwrap(); + let target = ManagedPolicyTarget { + session_id: sid, + sandbox_id: engram_core::SandboxId::new(), + host_id: HostId::new(), + binding_epoch: 1, + }; + let record = ManagedRecord { + version: 1, + context: ManagedPolicyContext { + target: target.clone(), + operator_generation: 1, + operator_identity: format!("sha256:{}", "a".repeat(64)), + binding_hash: None, + }, + egress: super::live_tests::egress(&target, 1, &["mcp.example"]), + applied: None, + expected: None, + accepted_host_incarnation: "host-a".into(), + accepted_proxy_incarnation: "proxy-a".into(), + }; + let bytes = crate::durable_envelope::seal( + &sid.to_string(), + &serde_json::to_string(&record).unwrap(), + ); + let marker = dir.path().join(format!("{sid}.managed-required")); + let path = dir.path().join(format!("{sid}.managed")); + std::fs::write(&marker, b"1").unwrap(); + assert!(read_record(&store, sid).is_err()); + for end in 0..bytes.len() { + std::fs::write(&path, &bytes[..end]).unwrap(); + assert!(read_record(&store, sid).is_err(), "offset {end}"); + } + std::fs::write(&path, &bytes).unwrap(); + assert!(read_record(&store, sid).unwrap().is_some()); + std::fs::write( + dir.path().join(format!("{sid}.managed.partial")), + b"garbage", + ) + .unwrap(); + assert!( + read_record(&store, sid).unwrap().is_some(), + "incomplete sibling cannot replace committed policy" + ); + std::fs::remove_file(&marker).unwrap(); + assert!( + read_record(&store, sid).is_err(), + "missing completeness marker cannot be accepted" + ); + } +} + +#[cfg(test)] +mod live_tests { + use super::*; + use engram_core::{ + types::{ + egress::SessionEgressPolicy, + endpoints::GuestEndpoints, + image::SecretMode, + sandbox::{ExecRequest, ExecStream, SandboxSpec}, + snapshot::SnapshotMetadata, + }, + SandboxId, SnapshotId, + }; + struct EndpointFixture { + egress: Arc, + } + #[async_trait::async_trait] + impl SandboxBackend for EndpointFixture { + async fn create(&self, _: SandboxSpec) -> Result { + Err(invalid("unused fixture")) + } + async fn exec_stream( + &self, + _: SandboxId, + _: ExecRequest, + ) -> Result { + Err(invalid("unused fixture")) + } + async fn snapshot(&self, _: SandboxId) -> Result { + Err(invalid("unused fixture")) + } + fn snapshot_path_for(&self, _: SnapshotId) -> std::path::PathBuf { + std::path::PathBuf::new() + } + async fn restore(&self, _: SnapshotMetadata) -> Result { + Err(invalid("unused fixture")) + } + async fn destroy(&self, _: SandboxId) -> Result<(), SandboxError> { + Ok(()) + } + async fn list(&self) -> Result, SandboxError> { + Ok(Vec::new()) + } + async fn guest_endpoints(&self, _: SandboxId) -> Option { + Some(GuestEndpoints { + egress_identity: "10.200.0.42".parse().unwrap(), + dial_ip: "10.200.0.42".parse().unwrap(), + netns: None, + vsock_uds: None, + }) + } + async fn notify_session_policy( + &self, + policy: SessionEgressPolicy, + ) -> Result<(), SandboxError> { + self.egress.apply_policy(policy).await.map_err(invalid) + } + } + pub(super) fn egress( + target: &ManagedPolicyTarget, + revision: u64, + hosts: &[&str], + ) -> SessionEgressPolicy { + SessionEgressPolicy { + session_id: target.session_id, + sandbox_id: target.sandbox_id, + guest_ip: "10.200.0.42".parse().unwrap(), + network_allow_hosts: vec![], + network_allow_host_patterns: vec![], + allow_all: true, + secrets: vec![], + injects: vec![], + observes: vec![], + guest_services: vec![], + tunnels: vec![], + secret_mode: SecretMode::Literal, + apps: vec![], + managed_deny: Some( + ManagedDenyPolicy::new(revision, hosts.iter().map(|s| s.to_string())).unwrap(), + ), + } + } + #[tokio::test] + async fn durable_cas_replays_pending_union_and_fences_receipts() { + tokio::time::timeout(std::time::Duration::from_secs(10), async { + let temp = tempfile::tempdir().unwrap(); + let dir = temp.path(); + let ca = engram_egress_proxy::Ca::load_or_generate(&dir.join("ca")).unwrap(); + let host_id = HostId::new(); + let mut args = engram_egress_proxyd::DaemonArgs::new( + dir.to_path_buf(), + ca.cert_pem.clone(), + ca.key_pair.serialize_pem(), + "http://127.0.0.1:1".into(), + None, + host_id, + ); + [args.proxy_port, args.dns_port, args.gateway_port] = + engram_testkit::net::free_tcp_udp_ports(); + let daemon = tokio::spawn(engram_egress_proxyd::run(args)); + let handle = Arc::new(crate::proxyd_client::ProxydHandle::new( + dir.join(engram_egress_proto::CONTROL_SOCK_NAME), + )); + loop { + if handle.hello().await.is_ok() { + break; + } + assert!(!daemon.is_finished(), "daemon stopped"); + tokio::task::yield_now().await; + } + let egress_host = Arc::new(HostEgress::new(handle.clone(), ca.cert_pem, None)); + let backend = Arc::new(EndpointFixture { + egress: egress_host.clone(), + }); + let bindings = BindingStore::open(dir.join("bindings")).unwrap(); + let ownership = ManagedOwnership::for_mode(bindings.directory(), true) + .unwrap() + .unwrap(); + let target = ManagedPolicyTarget { + session_id: SessionId::new(), + sandbox_id: SandboxId::new(), + host_id, + binding_epoch: 1, + }; + bindings + .bind(target.session_id, target.sandbox_id, 1) + .unwrap(); + let host = ManagedPolicyHost::new( + host_id, + "host-a".into(), + bindings.clone(), + backend.clone(), + egress_host.clone(), + ownership.clone(), + ) + .unwrap(); + let context = ManagedPolicyContext { + target: target.clone(), + operator_generation: 1, + operator_identity: format!("sha256:{}", "a".repeat(64)), + binding_hash: None, + }; + assert!(host.read_applied(&target).await.unwrap().is_none()); + let first = ManagedPolicyApply { + expected: None, + context: context.clone(), + egress: egress(&target, 1, &["mcp.example"]), + }; + let ack = host.compare_and_apply(first.clone()).await.unwrap(); + assert_eq!(host.compare_and_apply(first.clone()).await.unwrap(), ack); + let mut bad = first.clone(); + bad.expected = Some( + ManagedAppliedState::new( + context.clone(), + ack.policy.clone(), + "foreign".into(), + "foreign".into(), + None, + ) + .unwrap(), + ); + assert!(host.compare_and_apply(bad).await.is_err()); + assert_eq!( + handle + .decide(first.egress.guest_ip, "MCP.EXAMPLE.") + .await + .unwrap() + .as_deref(), + Some("reject") + ); + let second = ManagedPolicyApply { + expected: Some(ack.clone()), + context: context.clone(), + egress: egress(&target, 2, &["mcp.example", "alias.example"]), + }; + let ack2 = host.compare_and_apply(second.clone()).await.unwrap(); + assert!(host.compare_and_apply(first).await.is_err()); + let mut shrink = second.clone(); + shrink.expected = Some(ack2.clone()); + shrink.egress = egress(&target, 3, &["mcp.example"]); + assert!(host.compare_and_apply(shrink).await.is_err()); + // Construct the crash state: desired rename completed, proxy apply/ACK did not. + let pending = ManagedRecord { + version: 1, + context: context.clone(), + egress: egress(&target, 3, &["mcp.example", "alias.example", "new.example"]), + applied: None, + expected: Some(ack2.clone()), + accepted_host_incarnation: "host-a".into(), + accepted_proxy_incarnation: ack2.proxy_incarnation.clone(), + }; + persist(&bindings, &pending).unwrap(); + let successor = ManagedPolicyHost::new( + host_id, + "host-b".into(), + bindings.clone(), + backend, + egress_host, + ownership, + ) + .unwrap(); + successor.recover_all().await.unwrap(); + let recovered = successor.read_applied(&target).await.unwrap().unwrap(); + assert_eq!(recovered.policy.revision, 3); + assert_eq!(recovered.host_incarnation, "host-b"); + assert_eq!( + handle + .decide(pending.egress.guest_ip, "new.example") + .await + .unwrap() + .as_deref(), + Some("reject") + ); + let stale = ManagedPolicyApply { + expected: Some(ack2), + context: context.clone(), + egress: pending.egress.clone(), + }; + assert!(successor.compare_and_apply(stale).await.is_err()); + let current = ManagedPolicyApply { + expected: Some(recovered.clone()), + context, + egress: pending.egress, + }; + assert_eq!( + successor.compare_and_apply(current.clone()).await.unwrap(), + recovered + ); + let record_path = bindings + .directory() + .join(format!("{}.managed", target.session_id)); + let valid_bytes = std::fs::read(&record_path).unwrap(); + std::fs::remove_file(&record_path).unwrap(); + assert!(successor.recover_all().await.is_err()); + assert!(handle + .read_managed(target.session_id) + .await + .unwrap() + .is_none()); + std::fs::write(&record_path, b"corrupt").unwrap(); + assert!(successor.recover_all().await.is_err()); + assert!(handle + .read_managed(target.session_id) + .await + .unwrap() + .is_none()); + std::fs::write(&record_path, valid_bytes).unwrap(); + successor.recover_all().await.unwrap(); + let mut final_request = current.clone(); + final_request.context.binding_hash = Some(format!("sha256:{}", "b".repeat(64))); + final_request.egress = + egress(&target, 4, &["mcp.example", "alias.example", "new.example"]); + assert!(successor.compare_and_apply(final_request).await.is_err()); + assert!(successor.read_applied(&target).await.is_err()); + bindings + .bind(target.session_id, target.sandbox_id, 2) + .unwrap(); + assert!(successor.read_applied(&target).await.is_err()); + assert!(handle + .read_managed(target.session_id) + .await + .unwrap() + .is_none()); + std::fs::write( + bindings + .directory() + .join(format!("{}.managed", target.session_id)), + b"corrupt", + ) + .unwrap(); + let mut next = target.clone(); + next.binding_epoch = 2; + assert!(successor.read_applied(&next).await.is_err()); + let mut socket = + tokio::net::UnixStream::connect(dir.join(engram_egress_proto::CONTROL_SOCK_NAME)) + .await + .unwrap(); + engram_egress_proto::write_frame(&mut socket, &engram_egress_proto::ToProxyd::Shutdown) + .await + .unwrap(); + assert_eq!(daemon.await.unwrap(), 0); + }) + .await + .expect("bounded host policy test"); + } +} + +#[cfg(test)] +mod ownership_tests { + // Tests drive subprocesses; OS time is a bounded test input (ADR 0098 D1). + #![allow(clippy::disallowed_methods)] + use super::*; + #[test] + fn ownership_child() { + let Some(dir) = std::env::var_os("ENGRAM_MANAGED_OWNERSHIP_TEST_DIR") else { + return; + }; + let path = std::path::PathBuf::from(dir); + let _owner = ManagedOwnership::for_mode(&path, true).unwrap().unwrap(); + std::fs::write(path.join("test-ready"), b"ready").unwrap(); + loop { + std::thread::park(); + } + } + #[test] + fn subprocess_overlap_rejected_and_crash_releases_ownership() { + let temp = tempfile::tempdir().unwrap(); + let mut child = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "managed_policy::ownership_tests::ownership_child", + "--nocapture", + ]) + .env("ENGRAM_MANAGED_OWNERSHIP_TEST_DIR", temp.path()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + .unwrap(); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); + while !temp.path().join("test-ready").exists() { + if std::time::Instant::now() >= deadline || child.try_wait().unwrap().is_some() { + let _ = child.kill(); + let _ = child.wait(); + panic!("ownership child did not become ready"); + } + std::thread::sleep(std::time::Duration::from_millis(5)); + } + let rejected = ManagedOwnership::for_mode(temp.path(), true).is_err(); + child.kill().unwrap(); + child.wait().unwrap(); + assert!(rejected); + let _successor = ManagedOwnership::for_mode(temp.path(), true) + .unwrap() + .unwrap(); + assert!( + ManagedOwnership::for_mode(temp.path(), false).is_err(), + "plaintext mode cannot reopen a managed directory" + ); + } +} diff --git a/crates/engram-host-agent/src/managed_tls.rs b/crates/engram-host-agent/src/managed_tls.rs new file mode 100644 index 000000000..e3114646a --- /dev/null +++ b/crates/engram-host-agent/src/managed_tls.rs @@ -0,0 +1,35 @@ +//! Managed host mode authenticates the single lifecycle listener. +use serde::Deserialize; +use std::{collections::BTreeSet, path::PathBuf}; +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ManagedHostTls { + pub certificate: PathBuf, + pub key: PathBuf, + pub client_ca: PathBuf, + pub coordinator_pins: BTreeSet, +} +impl ManagedHostTls { + pub fn from_env() -> Result, String> { + let Some(path) = std::env::var_os("ENGRAM_HOST_MANAGED_TLS_CONFIG") else { + return Ok(None); + }; + let bytes = + std::fs::read(path).map_err(|_| "cannot read managed host TLS configuration")?; + let value: Self = + serde_json::from_slice(&bytes).map_err(|_| "invalid managed host TLS configuration")?; + engram_protocol::host_tls::validate_pins(&value.coordinator_pins)?; + Ok(Some(value)) + } + pub fn server_config(&self) -> Result { + engram_protocol::host_tls::validate_pins(&self.coordinator_pins)?; + engram_tls::install_provider(); + let cert = + std::fs::read(&self.certificate).map_err(|_| "cannot read managed host certificate")?; + let key = std::fs::read(&self.key).map_err(|_| "cannot read managed host key")?; + let ca = std::fs::read(&self.client_ca).map_err(|_| "cannot read coordinator CA")?; + Ok(tonic::transport::ServerTlsConfig::new() + .identity(tonic::transport::Identity::from_pem(cert, key)) + .client_ca_root(tonic::transport::Certificate::from_pem(ca))) + } +} diff --git a/crates/engram-host-agent/src/pooled_backend.rs b/crates/engram-host-agent/src/pooled_backend.rs index d6de02c58..ef174ca74 100644 --- a/crates/engram-host-agent/src/pooled_backend.rs +++ b/crates/engram-host-agent/src/pooled_backend.rs @@ -9967,6 +9967,12 @@ impl SandboxBackend for PooledBackend { self.inner.set_upload_sink(sink); } + async fn managed_network_proof( + &self, + target: &engram_core::types::managed_deny::ManagedPolicyTarget, + ) -> Option { + self.inner.managed_network_proof(target).await + } async fn notify_session_policy(&self, policy: SessionEgressPolicy) -> Result<(), SandboxError> { let sandbox_id = policy.sandbox_id; let session_id = policy.session_id; @@ -9987,7 +9993,10 @@ impl SandboxBackend for PooledBackend { self.session_bindings.insert(sandbox_id, session_id); let Some(egress) = self.egress.as_ref() else { - if !policy.guest_services.is_empty() || !policy.tunnels.is_empty() { + if policy.managed_deny.is_some() + || !policy.guest_services.is_empty() + || !policy.tunnels.is_empty() + { return Err(SandboxError::InvalidSpec( "guest services and tunnels require a host egress proxy".into(), )); @@ -11181,6 +11190,7 @@ mod tests { for (guest_services, tunnels) in cases { let error = pooled .notify_session_policy(SessionEgressPolicy { + managed_deny: None, session_id: SessionId::new(), sandbox_id: SandboxId::new(), guest_ip: std::net::Ipv4Addr::LOCALHOST, @@ -15598,6 +15608,7 @@ mod tests { allow: &[&str], ) -> SessionEgressPolicy { SessionEgressPolicy { + managed_deny: None, session_id, sandbox_id, guest_ip, diff --git a/crates/engram-host-agent/src/proxyd_client.rs b/crates/engram-host-agent/src/proxyd_client.rs index 28f231122..71a1f8978 100644 --- a/crates/engram-host-agent/src/proxyd_client.rs +++ b/crates/engram-host-agent/src/proxyd_client.rs @@ -121,11 +121,29 @@ impl ProxydHandle { } } - /// One request/response round-trip. Serialized behind the mutex — - /// the control plane is low-rate (policy applies and removals). - /// On any transport error the cached connection is dropped so the - /// next call redials (the respawn recovery path). + /// Managed requests own separate streams so a session drain cannot block + /// another session. Ordinary requests share the cached stream; an error + /// drops that connection so the next call redials. No request is retried. async fn request(&self, msg: &ToProxyd) -> Result { + let managed = matches!( + msg, + ToProxyd::ListManaged + | ToProxyd::ReadManaged(_) + | ToProxyd::QuarantineManaged(_) + | ToProxyd::Hello { .. } + ) || matches!(msg, ToProxyd::ApplyPolicy(policy) if policy.managed_deny.is_some()); + if managed { + // A session drain must not occupy another session's control stream. + let mut stream = tokio::net::UnixStream::connect(&self.control_sock) + .await + .map_err(|e| ProxydError(format!("managed control connect: {e}")))?; + engram_egress_proto::write_frame(&mut stream, msg) + .await + .map_err(|e| ProxydError(format!("managed control write: {e}")))?; + return engram_egress_proto::read_frame(&mut stream) + .await + .map_err(|e| ProxydError(format!("managed control read: {e}"))); + } let mut guard = self.conn.lock().await; if guard.is_none() { let stream = tokio::net::UnixStream::connect(&self.control_sock) @@ -170,6 +188,31 @@ impl ProxydHandle { } } + pub async fn managed_sessions(&self) -> Result, ProxydError> { + match self.request(&ToProxyd::ListManaged).await? { + FromProxyd::ManagedSessions(sessions) => Ok(sessions), + _ => Err(ProxydError("invalid managed enumeration response".into())), + } + } + pub async fn read_managed( + &self, + session: engram_core::SessionId, + ) -> Result, ProxydError> { + match self.request(&ToProxyd::ReadManaged(session)).await? { + FromProxyd::ManagedState(state) => Ok(state), + _ => Err(ProxydError("invalid managed policy response".into())), + } + } + pub async fn quarantine_managed( + &self, + session: engram_core::SessionId, + ) -> Result<(), ProxydError> { + self.expect_ok( + &ToProxyd::QuarantineManaged(session), + "quarantine managed session", + ) + .await + } pub async fn apply_policy( &self, policy: engram_core::types::egress::SessionEgressPolicy, diff --git a/crates/engram-host-agent/tests/e2e_harness.rs b/crates/engram-host-agent/tests/e2e_harness.rs index 6b91dfa17..0ddaadbf6 100644 --- a/crates/engram-host-agent/tests/e2e_harness.rs +++ b/crates/engram-host-agent/tests/e2e_harness.rs @@ -750,6 +750,7 @@ async fn e2e_harness_cold_via_pooled_backend() { let allow_list: Vec = ALLOW_HOSTS.iter().map(|s| s.to_string()).collect(); let network_allow = engram_egress_proxy::HostList::from_manifest(&allow_list, &[]).unwrap(); registry.register(engram_egress_proxy::SessionState { + managed_deny: None, session_id, guest_ip, network_allow, @@ -853,6 +854,7 @@ async fn e2e_harness_warm_via_pooled_backend() { let allow_list: Vec = ALLOW_HOSTS.iter().map(|s| s.to_string()).collect(); let network_allow = engram_egress_proxy::HostList::from_manifest(&allow_list, &[]).unwrap(); registry.register(engram_egress_proxy::SessionState { + managed_deny: None, session_id, guest_ip, network_allow, diff --git a/crates/engram-host-agent/tests/egress_stream_survives_roll.rs b/crates/engram-host-agent/tests/egress_stream_survives_roll.rs index 676f7d2b9..5848161f1 100644 --- a/crates/engram-host-agent/tests/egress_stream_survives_roll.rs +++ b/crates/engram-host-agent/tests/egress_stream_survives_roll.rs @@ -304,6 +304,7 @@ async fn established_guest_stream_survives_a_host_agent_roll() { // The production apply path: policy to the daemon + the ADR 0111 // persist the successor replays from. let policy = engram_core::types::egress::SessionEgressPolicy { + managed_deny: None, session_id: engram_core::SessionId::new(), sandbox_id: sandbox, guest_ip, diff --git a/crates/engram-host-agent/tests/egress_survives_roll.rs b/crates/engram-host-agent/tests/egress_survives_roll.rs index a01e5abff..79e720131 100644 --- a/crates/engram-host-agent/tests/egress_survives_roll.rs +++ b/crates/engram-host-agent/tests/egress_survives_roll.rs @@ -187,6 +187,7 @@ async fn survivor_keeps_egress_across_a_host_agent_roll() { let session_id = engram_core::SessionId::new(); let guest_ip = std::net::Ipv4Addr::new(10, 200, 0, 2); let policy = engram_core::types::egress::SessionEgressPolicy { + managed_deny: None, session_id, sandbox_id: sandbox, guest_ip, diff --git a/crates/engram-host-agent/tests/managed_tls.rs b/crates/engram-host-agent/tests/managed_tls.rs new file mode 100644 index 000000000..e3b33798f --- /dev/null +++ b/crates/engram-host-agent/tests/managed_tls.rs @@ -0,0 +1,450 @@ +//! Local TLS process proof; no guest or deployment is involved. +use engram_protocol::{ + grpc_client::GrpcHostClient, + host_tls::{HostClientTls, HostPeer}, +}; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, +}; +fn openssl(dir: &std::path::Path, args: &[&str]) { + let output = std::process::Command::new("openssl") + .current_dir(dir) + .args(args) + .output() + .unwrap(); + assert!(output.status.success(), "openssl fixture generation failed"); +} +#[tokio::test] +async fn managed_host_requires_pinned_mutual_tls() { + let temp = tempfile::tempdir().unwrap(); + let dir = temp.path(); + openssl( + dir, + &[ + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + "ca.key", + "-out", + "ca.pem", + "-days", + "1", + "-subj", + "/CN=test-ca", + ], + ); + std::fs::write( + dir.join("extensions"), + "subjectAltName=DNS:localhost\nextendedKeyUsage=serverAuth,clientAuth\n", + ) + .unwrap(); + for name in ["host", "coord", "other"] { + openssl( + dir, + &[ + "req", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + &format!("{name}.key"), + "-out", + &format!("{name}.csr"), + "-subj", + "/CN=localhost", + ], + ); + openssl( + dir, + &[ + "x509", + "-req", + "-in", + &format!("{name}.csr"), + "-CA", + "ca.pem", + "-CAkey", + "ca.key", + "-CAcreateserial", + "-out", + &format!("{name}.pem"), + "-days", + "1", + "-extfile", + "extensions", + ], + ); + openssl( + dir, + &[ + "x509", + "-in", + &format!("{name}.pem"), + "-outform", + "DER", + "-out", + &format!("{name}.der"), + ], + ); + } + let host_id = engram_core::HostId::new(); + use engram_core::{traits::HostClient, types::managed_deny::*}; + let ca = engram_egress_proxy::Ca::load_or_generate(&dir.join("proxy-ca")).unwrap(); + let mut args = engram_egress_proxyd::DaemonArgs::new( + dir.to_path_buf(), + ca.cert_pem.clone(), + ca.key_pair.serialize_pem(), + "http://127.0.0.1:1".into(), + None, + host_id, + ); + [args.proxy_port, args.dns_port, args.gateway_port] = engram_testkit::net::free_tcp_udp_ports(); + let daemon = tokio::spawn(engram_egress_proxyd::run(args)); + let handle = Arc::new(engram_host_agent::proxyd_client::ProxydHandle::new( + dir.join(engram_egress_proto::CONTROL_SOCK_NAME), + )); + tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + if handle.hello().await.is_ok() { + break; + } + assert!(!daemon.is_finished()); + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + let egress = Arc::new(engram_host_agent::egress::HostEgress::new( + handle, + ca.cert_pem, + None, + )); + let backend = Arc::new(EndpointFixture(egress.clone())); + let bindings = engram_host_agent::bindings::BindingStore::open(dir.join("bindings")).unwrap(); + let target = ManagedPolicyTarget { + session_id: engram_core::SessionId::new(), + sandbox_id: engram_core::SandboxId::new(), + host_id, + binding_epoch: 1, + }; + bindings + .bind(target.session_id, target.sandbox_id, target.binding_epoch) + .unwrap(); + let owner = + engram_host_agent::managed_policy::ManagedOwnership::for_mode(&dir.join("bindings"), true) + .unwrap() + .unwrap(); + let managed = Arc::new( + engram_host_agent::managed_policy::ManagedPolicyHost::new( + host_id, + "tls-test-host".into(), + bindings.clone(), + backend.clone(), + egress, + owner, + ) + .unwrap(), + ); + let hub = Arc::new(engram_host_agent::harness::HarnessHub::new( + engram_host_agent::harness::event_sink_to(|_, _, _, _| async { Ok(()) }), + bindings, + )); + let local = Arc::new( + engram_host_agent::host_client::LocalHostClient::new(backend, hub) + .with_managed_policy(managed), + ); + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + drop(listener); + let pins = |name: &str| { + BTreeSet::from([engram_protocol::host_tls::certificate_pin( + &std::fs::read(dir.join(format!("{name}.der"))).unwrap(), + )]) + }; + let server = tokio::spawn(engram_host_agent::grpc_server::boot_with_managed_tls( + addr, + local.clone(), + None, + engram_host_agent::session_epochs::SessionEpochStore::open(dir.join("epochs")).unwrap(), + None, + Some(engram_host_agent::managed_tls::ManagedHostTls { + certificate: dir.join("host.pem"), + key: dir.join("host.key"), + client_ca: dir.join("ca.pem"), + coordinator_pins: pins("coord"), + }), + )); + let address = format!("https://{addr}"); + let mut client = HostClientTls { + client_certificate: dir.join("coord.pem"), + client_key: dir.join("coord.key"), + hosts: BTreeMap::from([( + host_id, + HostPeer { + endpoint: address.clone(), + server_name: "localhost".into(), + server_ca: dir.join("ca.pem"), + server_pins: pins("host"), + }, + )]), + }; + let connect = |config: &HostClientTls| { + config + .endpoint( + host_id, + &address, + tonic::transport::Endpoint::from_shared(address.clone()).unwrap(), + ) + .unwrap() + .connect_lazy() + }; + tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + if GrpcHostClient::new(connect(&client)).ping().await.is_ok() { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + }) + .await + .unwrap(); + let remote = GrpcHostClient::new(connect(&client)); + assert!(remote + .read_managed_policy(target.clone()) + .await + .unwrap() + .is_none()); + let context = ManagedPolicyContext { + target: target.clone(), + operator_generation: 1, + operator_identity: format!("sha256:{}", "a".repeat(64)), + binding_hash: None, + }; + let policy = engram_core::types::egress::SessionEgressPolicy { + session_id: target.session_id, + sandbox_id: target.sandbox_id, + guest_ip: "10.200.0.42".parse().unwrap(), + network_allow_hosts: vec![], + network_allow_host_patterns: vec![], + allow_all: true, + secrets: vec![], + injects: vec![], + observes: vec![], + guest_services: vec![], + tunnels: vec![], + secret_mode: engram_core::types::image::SecretMode::Literal, + apps: vec![], + managed_deny: Some(ManagedDenyPolicy::new(1, ["mcp.example".into()]).unwrap()), + }; + let apply = ManagedPolicyApply { + context, + expected: None, + egress: policy, + }; + let ack = remote.apply_managed_policy(apply.clone()).await.unwrap(); + assert_eq!( + remote.read_managed_policy(target.clone()).await.unwrap(), + Some(ack.clone()) + ); + assert_eq!(remote.apply_managed_policy(apply).await.unwrap(), ack); + assert!( + ack.context.binding_hash.is_none() && ack.network_proof.is_none(), + "bootstrap is not final network proof" + ); + let snapshot: engram_core::types::snapshot::SnapshotMetadata = + serde_json::from_value(serde_json::json!({ + "id": engram_core::SnapshotId::new(), "size_bytes": 0, + "created_at": "2026-01-01T00:00:00Z", "image_version": "fixture", + })) + .unwrap(); + let managed_fence = engram_core::traits::SessionFence::new(target.session_id, 1); + let ordinary_fence = engram_core::traits::SessionFence::new(engram_core::SessionId::new(), 1); + for host in [&*local as &dyn HostClient, &remote as &dyn HostClient] { + assert!(host.restore(snapshot.clone(), ordinary_fence).await.is_ok()); + assert!(host + .restore_base_for_session(snapshot.clone(), Default::default(), vec![], ordinary_fence) + .await + .is_ok()); + assert!(host + .resume(engram_core::SandboxId::new(), ordinary_fence) + .await + .is_ok()); + for error in [ + host.resume(target.sandbox_id, ordinary_fence) + .await + .unwrap_err(), + host.restore(snapshot.clone(), managed_fence) + .await + .unwrap_err(), + host.restore_base_for_session( + snapshot.clone(), + Default::default(), + vec![], + managed_fence, + ) + .await + .unwrap_err(), + host.resume(target.sandbox_id, managed_fence) + .await + .unwrap_err(), + ] { + assert!( + error + .to_string() + .contains("managed restore quarantine unavailable"), + "{error}" + ); + } + } + // Missing desired state and a corrupt marker still identify a known managed session. + std::fs::remove_file( + dir.join("bindings") + .join(format!("{}.managed", target.session_id)), + ) + .unwrap(); + std::fs::write( + dir.join("bindings") + .join(format!("{}.managed-required", target.session_id)), + b"corrupt", + ) + .unwrap(); + assert!(remote + .restore(snapshot, managed_fence) + .await + .unwrap_err() + .to_string() + .contains("managed restore quarantine unavailable")); + client.client_certificate = dir.join("other.pem"); + client.client_key = dir.join("other.key"); + assert!( + GrpcHostClient::new(connect(&client)).ping().await.is_err(), + "same CA does not replace caller pins" + ); + client.client_certificate = dir.join("coord.pem"); + client.client_key = dir.join("coord.key"); + client.hosts.get_mut(&host_id).unwrap().server_pins = pins("other"); + assert!( + GrpcHostClient::new(connect(&client)).ping().await.is_err(), + "server pin required as well as CA/name" + ); + assert!(client + .endpoint( + host_id, + &format!("http://{addr}"), + tonic::transport::Endpoint::from_shared(format!("http://{addr}")).unwrap() + ) + .is_err()); + let plaintext = tonic::transport::Endpoint::from_shared(format!("http://{addr}")) + .unwrap() + .connect_lazy(); + assert!(GrpcHostClient::new(plaintext).ping().await.is_err()); + client.hosts.get_mut(&host_id).unwrap().server_pins = pins("host"); + client.hosts.get_mut(&host_id).unwrap().server_name = "wrong.example".into(); + assert!(GrpcHostClient::new(connect(&client)).ping().await.is_err()); + client.hosts.get_mut(&host_id).unwrap().server_name = "localhost".into(); + client.hosts.get_mut(&host_id).unwrap().server_ca = dir.join("coord.pem"); + assert!(GrpcHostClient::new(connect(&client)).ping().await.is_err()); + client.hosts.get_mut(&host_id).unwrap().server_ca = dir.join("ca.pem"); + let mut raw = + engram_protocol::grpc::host_service_client::HostServiceClient::new(connect(&client)); + assert!( + raw.ping(engram_protocol::grpc::Empty {}).await.is_err(), + "missing protocol version rejected" + ); + let mut old = tonic::Request::new(engram_protocol::grpc::Empty {}); + old.metadata_mut().insert( + engram_protocol::wire::WIRE_VERSION_METADATA_KEY, + "1".parse().unwrap(), + ); + assert!(raw.ping(old).await.is_err(), "old protocol rejected"); + let no_identity = tonic::transport::Endpoint::from_shared(address.clone()) + .unwrap() + .tls_config( + tonic::transport::ClientTlsConfig::new() + .domain_name("localhost") + .ca_certificate(tonic::transport::Certificate::from_pem( + std::fs::read(dir.join("ca.pem")).unwrap(), + )), + ) + .unwrap() + .connect_lazy(); + assert!(GrpcHostClient::new(no_identity).ping().await.is_err()); + daemon.abort(); + let _ = daemon.await; + server.abort(); + let _ = server.await; +} + +struct EndpointFixture(Arc); +#[async_trait::async_trait] +impl engram_core::traits::SandboxBackend for EndpointFixture { + async fn create( + &self, + _: engram_core::types::sandbox::SandboxSpec, + ) -> Result { + unimplemented!("fixture does not create guests") + } + async fn exec_stream( + &self, + _: engram_core::SandboxId, + _: engram_core::types::sandbox::ExecRequest, + ) -> Result { + unimplemented!("fixture does not execute") + } + async fn snapshot( + &self, + _: engram_core::SandboxId, + ) -> Result { + unimplemented!("fixture does not snapshot") + } + fn snapshot_path_for(&self, _: engram_core::SnapshotId) -> std::path::PathBuf { + std::path::PathBuf::new() + } + async fn restore( + &self, + _: engram_core::types::snapshot::SnapshotMetadata, + ) -> Result { + Ok(engram_core::SandboxId::new()) + } + async fn restore_base_for_session( + &self, + _: engram_core::types::snapshot::SnapshotMetadata, + _: std::collections::HashMap, + _: Vec, + ) -> Result { + Ok(engram_core::SandboxId::new()) + } + async fn resume(&self, _: engram_core::SandboxId) -> Result<(), engram_core::SandboxError> { + Ok(()) + } + async fn destroy(&self, _: engram_core::SandboxId) -> Result<(), engram_core::SandboxError> { + Ok(()) + } + async fn list(&self) -> Result, engram_core::SandboxError> { + Ok(vec![]) + } + async fn guest_endpoints( + &self, + _: engram_core::SandboxId, + ) -> Option { + Some(engram_core::types::endpoints::GuestEndpoints { + egress_identity: "10.200.0.42".parse().unwrap(), + dial_ip: "10.200.0.42".parse().unwrap(), + netns: None, + vsock_uds: None, + }) + } + async fn notify_session_policy( + &self, + policy: engram_core::types::egress::SessionEgressPolicy, + ) -> Result<(), engram_core::SandboxError> { + self.0 + .apply_policy(policy) + .await + .map_err(|e| engram_core::SandboxError::InvalidSpec(e.to_string())) + } +} diff --git a/crates/engram-postgres/src/lib.rs b/crates/engram-postgres/src/lib.rs index 9da01ae83..9bc31e118 100644 --- a/crates/engram-postgres/src/lib.rs +++ b/crates/engram-postgres/src/lib.rs @@ -8225,24 +8225,79 @@ impl MetadataStore for PostgresStore { // ---- session integration policy (ADR 0056 B′) ---- + async fn managed_policy_target( + &self, + session_id: SessionId, + ) -> Result { + use sqlx::Row; + let row = sqlx::query("SELECT host_id, sandbox_id, binding_epoch FROM sessions WHERE id=$1 AND host_id IS NOT NULL AND sandbox_id IS NOT NULL AND binding_epoch > 0 AND status NOT IN ('dead','failed','completed')") + .bind(session_id.as_uuid()).fetch_optional(&self.pool).await.map_err(db_err)?.ok_or(MetaError::NotFound)?; + Ok(engram_core::types::managed_deny::ManagedPolicyTarget { + session_id, + host_id: HostId(row.try_get("host_id").map_err(db_err)?), + sandbox_id: SandboxId(row.try_get("sandbox_id").map_err(db_err)?), + binding_epoch: u64::try_from(row.try_get::("binding_epoch").map_err(db_err)?) + .map_err(|e| MetaError::Serialization(e.to_string()))?, + }) + } + async fn retain_managed_deny( + &self, + target: &engram_core::types::managed_deny::ManagedPolicyTarget, + policy: &engram_core::types::managed_deny::ManagedDenyPolicy, + ) -> Result<(), MetaError> { + use sqlx::Row; + let mut tx = self.pool.begin().await.map_err(db_err)?; + let row = sqlx::query("SELECT host_id, sandbox_id, binding_epoch FROM sessions WHERE id=$1 AND status NOT IN ('dead','failed','completed') FOR UPDATE") + .bind(target.session_id.as_uuid()).fetch_optional(&mut *tx).await.map_err(db_err)?.ok_or(MetaError::NotFound)?; + if row + .try_get::, _>("host_id") + .map_err(db_err)? + != Some(target.host_id.as_uuid()) + || row + .try_get::, _>("sandbox_id") + .map_err(db_err)? + != Some(target.sandbox_id.as_uuid()) + || row.try_get::("binding_epoch").map_err(db_err)? + != i64::try_from(target.binding_epoch) + .map_err(|e| MetaError::Serialization(e.to_string()))? + { + return Err(MetaError::Conflict("managed placement changed".into())); + } + let old: Option = sqlx::query_scalar( + "SELECT policy_json FROM session_integration_policy WHERE session_id=$1", + ) + .bind(target.session_id.as_uuid()) + .fetch_optional(&mut *tx) + .await + .map_err(db_err)?; + let next = engram_core::types::managed_deny::retain_in_policy_json(old.as_deref(), policy) + .map_err(MetaError::Conflict)?; + sqlx::query("INSERT INTO session_integration_policy (session_id,policy_json) VALUES ($1,$2) ON CONFLICT (session_id) DO UPDATE SET policy_json=EXCLUDED.policy_json").bind(target.session_id.as_uuid()).bind(next).execute(&mut *tx).await.map_err(db_err)?; + tx.commit().await.map_err(db_err) + } async fn bind_session_integration_policy( &self, session_id: SessionId, policy_json: &str, ) -> Result<(), MetaError> { - sqlx::query( - r#" - INSERT INTO session_integration_policy (session_id, policy_json) - VALUES ($1, $2) - ON CONFLICT (session_id) DO UPDATE SET policy_json = EXCLUDED.policy_json - "#, + let mut tx = self.pool.begin().await.map_err(db_err)?; + sqlx::query("SELECT id FROM sessions WHERE id=$1 FOR UPDATE") + .bind(session_id.as_uuid()) + .fetch_optional(&mut *tx) + .await + .map_err(db_err)? + .ok_or(MetaError::NotFound)?; + let old: Option = sqlx::query_scalar( + "SELECT policy_json FROM session_integration_policy WHERE session_id=$1", ) .bind(session_id.as_uuid()) - .bind(policy_json) - .execute(&self.pool) + .fetch_optional(&mut *tx) .await .map_err(db_err)?; - Ok(()) + engram_core::types::managed_deny::validate_policy_replacement(old.as_deref(), policy_json) + .map_err(MetaError::Conflict)?; + sqlx::query("INSERT INTO session_integration_policy (session_id,policy_json) VALUES ($1,$2) ON CONFLICT (session_id) DO UPDATE SET policy_json=EXCLUDED.policy_json").bind(session_id.as_uuid()).bind(policy_json).execute(&mut *tx).await.map_err(db_err)?; + tx.commit().await.map_err(db_err) } async fn get_session_integration_policy( diff --git a/crates/engram-protocol/Cargo.toml b/crates/engram-protocol/Cargo.toml index e0cc7061e..962b00480 100644 --- a/crates/engram-protocol/Cargo.toml +++ b/crates/engram-protocol/Cargo.toml @@ -10,6 +10,11 @@ description = "Wire types for Engram coordinator<->host communication." workspace = true [dependencies] +engram-tls.workspace = true +rustls.workspace = true +rustls-pemfile.workspace = true +sha2.workspace = true +hex.workspace = true engram-core.workspace = true engram-telemetry.workspace = true async-stream.workspace = true diff --git a/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto b/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto index 62ad9aa0c..64198593b 100644 --- a/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto +++ b/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto @@ -5,6 +5,8 @@ import "engram/app/v1/session.proto"; // Dedicated pinned mTLS only. Create reuses the ordinary creation core. service SessionPolicyService { + rpc ReadManagedPolicy(ReadManagedPolicyRequest) returns (ReadManagedPolicyResponse); + rpc ApplyManagedPolicy(ApplyManagedPolicyRequest) returns (ApplyManagedPolicyResponse); rpc CreatePolicySession(CreatePolicySessionRequest) returns (CreatePolicySessionResponse); rpc ReadWritePolicy(ReadWritePolicyRequest) returns (ReadWritePolicyResponse); rpc AcceptPlanExit(AcceptPlanExitRequest) returns (AcceptPlanExitResponse); @@ -70,3 +72,61 @@ message ValidateExecutionIdentityResponse { uint32 protocol = 1; string status = 2; } + +// These messages describe a host receipt, not permission to expose guest tools. +enum ManagedPolicyPhase { + MANAGED_POLICY_PHASE_UNSPECIFIED = 0; + MANAGED_POLICY_PHASE_BOOTSTRAP = 1; + MANAGED_POLICY_PHASE_FINAL = 2; +} +message ManagedPolicyTarget { + string session_id = 1; + string sandbox_id = 2; + string host_id = 3; + uint64 binding_epoch = 4; +} +message ManagedPolicyContext { + ManagedPolicyTarget target = 1; + uint64 operator_generation = 2; + string operator_identity = 3; + ManagedPolicyPhase phase = 4; + optional string binding_hash = 5; +} +message ManagedDenyPolicy { + uint32 version = 1; + uint64 revision = 2; + repeated string hosts = 3; + string digest = 4; +} +message ManagedAppliedState { + ManagedPolicyContext context = 1; + ManagedDenyPolicy policy = 2; + string host_incarnation = 3; + string proxy_incarnation = 4; + string state_id = 5; + optional ManagedNetworkProof network_proof = 6; +} +message ReadManagedPolicyRequest { + uint32 protocol = 1; + ManagedPolicyTarget target = 2; +} +message ReadManagedPolicyResponse { + uint32 protocol = 1; + optional ManagedAppliedState applied = 2; +} +message ApplyManagedPolicyRequest { + uint32 protocol = 1; + ManagedPolicyContext context = 2; + ManagedDenyPolicy policy = 3; + optional ManagedAppliedState expected = 4; +} +message ApplyManagedPolicyResponse { + uint32 protocol = 1; + ManagedAppliedState applied = 2; +} + +message ManagedNetworkProof { + ManagedPolicyTarget target = 1; + string guest_ip = 2; + string network_identity = 3; +} diff --git a/crates/engram-protocol/proto/host_service.proto b/crates/engram-protocol/proto/host_service.proto index 659e92f4a..34fe183f6 100644 --- a/crates/engram-protocol/proto/host_service.proto +++ b/crates/engram-protocol/proto/host_service.proto @@ -18,6 +18,8 @@ syntax = "proto3"; package engram.host.v1; service HostService { + rpc ReadManagedPolicy(ReadManagedPolicyRequest) returns (ReadManagedPolicyResponse); + rpc ApplyManagedPolicy(ApplyManagedPolicyRequest) returns (ApplyManagedPolicyResponse); // ---- sandbox lifecycle ---- // // ADR 0079 (wire v12): every session-scoped lifecycle RPC carries a @@ -866,3 +868,9 @@ message FlushSandboxResponse { // non-empty encodes `Some(ManifestRef { manifest_id, version })`. bytes manifest_ref_bincode = 1; } + +// These operations are served only in mandatory managed mTLS mode. +message ReadManagedPolicyRequest { bytes target_bincode = 1; } +message ReadManagedPolicyResponse { bytes state_bincode = 1; } +message ApplyManagedPolicyRequest { bytes request_bincode = 1; } +message ApplyManagedPolicyResponse { bytes state_bincode = 1; } diff --git a/crates/engram-protocol/src/grpc_client.rs b/crates/engram-protocol/src/grpc_client.rs index ba273c95c..c67cef921 100644 --- a/crates/engram-protocol/src/grpc_client.rs +++ b/crates/engram-protocol/src/grpc_client.rs @@ -1686,6 +1686,38 @@ impl HostClient for GrpcHostClient { Self::restore_base_for_session(self, metadata, session_env, selected_mounts, fence).await } + async fn read_managed_policy( + &self, + target: engram_core::types::managed_deny::ManagedPolicyTarget, + ) -> Result, SandboxError> { + let response = self + .inner + .clone() + .read_managed_policy(crate::grpc::ReadManagedPolicyRequest { + target_bincode: encode_bincode(&target, "managed target")?, + }) + .await + .map_err(grpc_to_sandbox_err)? + .into_inner(); + crate::managed_policy::decode(&response.state_bincode) + .map_err(|_| SandboxError::InvalidSpec("invalid managed state".into())) + } + async fn apply_managed_policy( + &self, + request: engram_core::types::managed_deny::ManagedPolicyApply, + ) -> Result { + let response = self + .inner + .clone() + .apply_managed_policy(crate::grpc::ApplyManagedPolicyRequest { + request_bincode: encode_bincode(&request, "managed apply")?, + }) + .await + .map_err(grpc_to_sandbox_err)? + .into_inner(); + crate::managed_policy::decode(&response.state_bincode) + .map_err(|_| SandboxError::InvalidSpec("invalid managed state".into())) + } async fn start_agent( &self, id: SandboxId, diff --git a/crates/engram-protocol/src/grpc_pool.rs b/crates/engram-protocol/src/grpc_pool.rs index a3ba1737b..c53ed5efd 100644 --- a/crates/engram-protocol/src/grpc_pool.rs +++ b/crates/engram-protocol/src/grpc_pool.rs @@ -30,6 +30,7 @@ use crate::grpc_client::GrpcHostClient; /// coordinator; each coord pod gets its own. pub struct GrpcHostPool { entries: DashMap, + tls: Option, /// HTTP/2 keepalive interval. Default 15s — short enough that /// idle connections stay healthy through L4-LB / NAT timeouts, /// long enough that the chatter is invisible in production. @@ -60,12 +61,18 @@ impl GrpcHostPool { pub fn new() -> Self { Self { entries: DashMap::new(), + tls: None, keepalive_interval: Duration::from_secs(15), keepalive_timeout: Duration::from_secs(5), connect_timeout: Duration::from_secs(2), } } + pub fn with_tls(mut self, tls: crate::host_tls::HostClientTls) -> Self { + self.tls = Some(tls); + self + } + /// Idempotent warm-up: ensure a `Channel` exists for `host_id` /// and that the underlying TCP+H2 handshake is in flight or /// complete. @@ -77,9 +84,9 @@ impl GrpcHostPool { /// - coord-pod startup — preloads pool entries for every /// `hosts` row with `status IN ('ready','draining')` /// - /// `host_addr` example: `http://10.10.0.42:9101`. Plain HTTP/2 - /// (h2c) — TLS off, bearer-token auth happens per-RPC via tonic - /// interceptors that the call sites attach. + /// Managed mode requires the configured HTTPS endpoint, CA, server name, + /// leaf pins, and client identity. Ordinary development can use HTTP/2 + /// without TLS. An HTTPS address never falls back to plaintext. pub async fn warm(&self, host_id: HostId, host_addr: String) -> Result<(), SandboxError> { // If we already have an entry for this addr, leave it alone. // If the addr changed (host re-registered with a fresh IP @@ -101,6 +108,17 @@ impl GrpcHostPool { self.keepalive_interval, self.keepalive_timeout, )?; + let endpoint = match &self.tls { + Some(tls) => tls + .endpoint(host_id, &host_addr, endpoint) + .map_err(SandboxError::InvalidSpec)?, + None if host_addr.starts_with("https://") => { + return Err(SandboxError::InvalidSpec( + "host TLS identity is not configured".into(), + )) + } + None => endpoint, + }; // `connect_lazy` returns immediately; the first RPC pays the // TCP+H2 handshake. We follow up with `ping` below to force // the handshake before the first real RPC. diff --git a/crates/engram-protocol/src/host_tls.rs b/crates/engram-protocol/src/host_tls.rs new file mode 100644 index 000000000..a7eb25ab4 --- /dev/null +++ b/crates/engram-protocol/src/host_tls.rs @@ -0,0 +1,141 @@ +//! Operator-owned, host-specific mTLS configuration. No plaintext fallback. +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use std::{ + collections::{BTreeMap, BTreeSet}, + path::PathBuf, + sync::Arc, +}; + +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HostPeer { + pub endpoint: String, + pub server_name: String, + pub server_ca: PathBuf, + pub server_pins: BTreeSet, +} +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct HostClientTls { + pub client_certificate: PathBuf, + pub client_key: PathBuf, + pub hosts: BTreeMap, +} +pub fn certificate_pin(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} +pub fn validate_pins(pins: &BTreeSet) -> Result<(), String> { + if pins.is_empty() + || pins.len() > 8 + || pins.iter().any(|p| { + p.len() != 64 + || !p + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) + }) + { + return Err("invalid host certificate pins".into()); + } + Ok(()) +} +#[derive(Debug)] +struct PinnedVerifier { + chain: Arc, + pins: BTreeSet, +} +impl ServerCertVerifier for PinnedVerifier { + fn verify_server_cert( + &self, + end: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + name: &ServerName<'_>, + ocsp: &[u8], + now: UnixTime, + ) -> Result { + let verified = self + .chain + .verify_server_cert(end, intermediates, name, ocsp, now)?; + if !self.pins.contains(&certificate_pin(end.as_ref())) { + return Err(rustls::Error::General( + "host certificate pin mismatch".into(), + )); + } + Ok(verified) + } + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + sig: &rustls::DigitallySignedStruct, + ) -> Result { + self.chain.verify_tls12_signature(message, cert, sig) + } + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + sig: &rustls::DigitallySignedStruct, + ) -> Result { + self.chain.verify_tls13_signature(message, cert, sig) + } + fn supported_verify_schemes(&self) -> Vec { + self.chain.supported_verify_schemes() + } +} +impl HostClientTls { + pub fn from_env() -> Result, String> { + let Some(path) = std::env::var_os("ENGRAM_HOST_CLIENT_TLS_CONFIG") else { + return Ok(None); + }; + let bytes = std::fs::read(path).map_err(|_| "cannot read host TLS configuration")?; + serde_json::from_slice(&bytes) + .map(Some) + .map_err(|_| "invalid host TLS configuration".into()) + } + pub fn endpoint( + &self, + host: engram_core::HostId, + address: &str, + endpoint: tonic::transport::Endpoint, + ) -> Result { + engram_tls::install_provider(); + let peer = self + .hosts + .get(&host) + .ok_or("host TLS identity is not configured")?; + if peer.endpoint != address + || !address.starts_with("https://") + || peer.server_name.is_empty() + { + return Err("host TLS endpoint mismatch".into()); + } + validate_pins(&peer.server_pins)?; + let ca = std::fs::read(&peer.server_ca).map_err(|_| "cannot read host CA")?; + let mut roots = rustls::RootCertStore::empty(); + for cert in rustls_pemfile::certs(&mut &ca[..]) { + roots + .add(cert.map_err(|_| "invalid host CA")?) + .map_err(|_| "invalid host CA")?; + } + let chain = rustls::client::WebPkiServerVerifier::builder(Arc::new(roots)) + .build() + .map_err(|_| "invalid host CA")?; + let certificate = std::fs::read(&self.client_certificate) + .map_err(|_| "cannot read coordinator certificate")?; + let key = std::fs::read(&self.client_key).map_err(|_| "cannot read coordinator TLS key")?; + endpoint + .tls_config_with_verifier( + tonic::transport::ClientTlsConfig::new() + .domain_name(&peer.server_name) + .identity(tonic::transport::Identity::from_pem(certificate, key)), + Arc::new(PinnedVerifier { + chain, + pins: peer.server_pins.clone(), + }), + ) + .map_err(|_| "invalid host TLS configuration".into()) + } +} diff --git a/crates/engram-protocol/src/lib.rs b/crates/engram-protocol/src/lib.rs index 7efaf4cd6..365afc4a8 100644 --- a/crates/engram-protocol/src/lib.rs +++ b/crates/engram-protocol/src/lib.rs @@ -27,3 +27,7 @@ pub use heartbeat::*; pub use wire::{WireExecRequest, WireReapStats, WIRE_VERSION}; pub mod mcp; + +pub mod host_tls; + +pub mod managed_policy; diff --git a/crates/engram-protocol/src/managed_policy.rs b/crates/engram-protocol/src/managed_policy.rs new file mode 100644 index 000000000..280f0c76c --- /dev/null +++ b/crates/engram-protocol/src/managed_policy.rs @@ -0,0 +1,178 @@ +//! Strict conversions for the dedicated managed policy control service. +use crate::mcp as wire; +use engram_core::types::managed_deny as core; +fn missing() -> String { + "required managed policy field absent".into() +} +impl TryFrom for core::ManagedPolicyTarget { + type Error = String; + fn try_from(v: wire::ManagedPolicyTarget) -> Result { + let target = Self { + session_id: v + .session_id + .parse() + .map_err(|_| "invalid session identity")?, + sandbox_id: v + .sandbox_id + .parse() + .map_err(|_| "invalid sandbox identity")?, + host_id: v.host_id.parse().map_err(|_| "invalid host identity")?, + binding_epoch: v.binding_epoch, + }; + if target.session_id.as_uuid().is_nil() + || target.sandbox_id.as_uuid().is_nil() + || target.host_id.as_uuid().is_nil() + || target.binding_epoch == 0 + || target.binding_epoch > 9_007_199_254_740_991 + { + return Err("invalid physical placement".into()); + } + Ok(target) + } +} +impl From for wire::ManagedPolicyTarget { + fn from(v: core::ManagedPolicyTarget) -> Self { + Self { + session_id: v.session_id.to_string(), + sandbox_id: v.sandbox_id.to_string(), + host_id: v.host_id.to_string(), + binding_epoch: v.binding_epoch, + } + } +} +impl TryFrom for core::ManagedPolicyContext { + type Error = String; + fn try_from(v: wire::ManagedPolicyContext) -> Result { + match (wire::ManagedPolicyPhase::try_from(v.phase), &v.binding_hash) { + (Ok(wire::ManagedPolicyPhase::Bootstrap), None) + | (Ok(wire::ManagedPolicyPhase::Final), Some(_)) => {} + _ => return Err("managed policy phase and binding hash disagree".into()), + } + let value = Self { + target: v.target.ok_or_else(missing)?.try_into()?, + operator_generation: v.operator_generation, + operator_identity: v.operator_identity, + binding_hash: v.binding_hash, + }; + value.validate()?; + Ok(value) + } +} +impl From for wire::ManagedPolicyContext { + fn from(v: core::ManagedPolicyContext) -> Self { + Self { + phase: if v.binding_hash.is_some() { + wire::ManagedPolicyPhase::Final as i32 + } else { + wire::ManagedPolicyPhase::Bootstrap as i32 + }, + target: Some(v.target.into()), + operator_generation: v.operator_generation, + operator_identity: v.operator_identity, + binding_hash: v.binding_hash, + } + } +} +impl TryFrom for core::ManagedDenyPolicy { + type Error = String; + fn try_from(v: wire::ManagedDenyPolicy) -> Result { + let value = Self { + version: v.version, + revision: v.revision, + hosts: v.hosts, + digest: v.digest, + }; + value.validate()?; + Ok(value) + } +} +impl From for wire::ManagedDenyPolicy { + fn from(v: core::ManagedDenyPolicy) -> Self { + Self { + version: v.version, + revision: v.revision, + hosts: v.hosts, + digest: v.digest, + } + } +} +impl TryFrom for core::ManagedAppliedState { + type Error = String; + fn try_from(v: wire::ManagedAppliedState) -> Result { + let value = Self { + context: v.context.ok_or_else(missing)?.try_into()?, + policy: v.policy.ok_or_else(missing)?.try_into()?, + host_incarnation: v.host_incarnation, + proxy_incarnation: v.proxy_incarnation, + state_id: v.state_id, + network_proof: v.network_proof.map(TryInto::try_into).transpose()?, + }; + value.validate()?; + Ok(value) + } +} +impl From for wire::ManagedAppliedState { + fn from(v: core::ManagedAppliedState) -> Self { + Self { + context: Some(v.context.into()), + policy: Some(v.policy.into()), + host_incarnation: v.host_incarnation, + proxy_incarnation: v.proxy_incarnation, + state_id: v.state_id, + network_proof: v.network_proof.map(Into::into), + } + } +} +impl TryFrom for core::ManagedNetworkProof { + type Error = String; + fn try_from(v: wire::ManagedNetworkProof) -> Result { + Ok(Self { + target: v.target.ok_or_else(missing)?.try_into()?, + guest_ip: v.guest_ip.parse().map_err(|_| "invalid guest identity")?, + network_identity: v.network_identity, + }) + } +} +impl From for wire::ManagedNetworkProof { + fn from(v: core::ManagedNetworkProof) -> Self { + Self { + target: Some(v.target.into()), + guest_ip: v.guest_ip.to_string(), + network_identity: v.network_identity, + } + } +} + +/// The managed bincode surface is bounded and rejects unknown trailing fields. +pub fn decode(bytes: &[u8]) -> Result { + use bincode::Options; + bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(4 * 1024 * 1024) + .reject_trailing_bytes() + .deserialize(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn unknown_phase_and_false_final_ack_are_closed() { + for (phase, binding_hash) in [ + (0, None), + (999, None), + (2, None), + (1, Some("sha256:invalid".into())), + (2, Some(String::new())), + ] { + assert!( + core::ManagedPolicyContext::try_from(wire::ManagedPolicyContext { + phase, + binding_hash, + ..Default::default() + }) + .is_err() + ); + } + } +} diff --git a/crates/engram-protocol/src/wire.rs b/crates/engram-protocol/src/wire.rs index 224824d3b..c8c2e6479 100644 --- a/crates/engram-protocol/src/wire.rs +++ b/crates/engram-protocol/src/wire.rs @@ -171,7 +171,7 @@ use serde::{Deserialize, Serialize}; // the Dead budget instead of recovering from disk. // v36 gates page-channel v6 fault windows for a lockstep host roll. // v37 appends HostCapabilities.page_idle and the optional MigrationItem::HotHint. -pub const WIRE_VERSION: u32 = 37; +pub const WIRE_VERSION: u32 = 38; /// gRPC metadata (header) key carrying the caller's [`WIRE_VERSION`] on /// every coord→host request (issue #229). ASCII, lowercase — tonic diff --git a/crates/engram-protocol/tests/decode_never_panics.rs b/crates/engram-protocol/tests/decode_never_panics.rs index b54930414..5636d252c 100644 --- a/crates/engram-protocol/tests/decode_never_panics.rs +++ b/crates/engram-protocol/tests/decode_never_panics.rs @@ -31,6 +31,12 @@ use support::{wire_exec_request, wire_reap_stats}; // ---- helpers ----------------------------------------------------------- fn decode_every_type(bytes: &[u8]) { + let _ = engram_protocol::managed_policy::decode::< + engram_core::types::managed_deny::ManagedPolicyApply, + >(bytes); + let _ = engram_protocol::managed_policy::decode::< + Option, + >(bytes); // Crate-local payloads. let _ = bincode::deserialize::(bytes); let _ = bincode::deserialize::(bytes); diff --git a/crates/engram-protocol/tests/golden/session_egress_policy.bin b/crates/engram-protocol/tests/golden/session_egress_policy.bin index 5509a6b839093575f25371129525252682a8cb0f..15156f08128840fc963789c56b6bc3fbb36e1923 100644 GIT binary patch delta 9 QcmeBV>SEf!$;ik601QSWr$$p`=lu>sNm diff --git a/crates/engram-protocol/tests/golden/session_egress_policy_google.bin b/crates/engram-protocol/tests/golden/session_egress_policy_google.bin index f82cf92824759dbb0bbbc7910848818164c75f0e..b52799ccbc5da0b994bdc3d5e7dd71c07678ebc3 100644 GIT binary patch delta 9 QcmdnWx{Y>$`Y85PKjWrZk=nvy0E5Z5l2)YKx}qrI95}rjH!T diff --git a/crates/engram-protocol/tests/golden/session_egress_policy_oauth.bin b/crates/engram-protocol/tests/golden/session_egress_policy_oauth.bin index 69dcfd2cc7a5e9fe1b00a9ed22e07a83563f6795..5b102538247afe90030de1030fe964c21d135d9c 100644 GIT binary patch delta 9 Qcmeyw{F!;fCq_mF02Y@6kN^Mx delta 7 Ocmey&{E2zPCq@7dc>|9C diff --git a/crates/engram-protocol/tests/golden/session_egress_policy_oauth_user.bin b/crates/engram-protocol/tests/golden/session_egress_policy_oauth_user.bin index 236d5811473740790bff2ad02c6dac9cf93aa799..38088e45133f553c3788a3296fe6c11ac1afdfb9 100644 GIT binary patch delta 9 QcmZo-X=d5L#Kgz|01M~=!vFvP delta 7 OcmZo>X=2&H!~_5cWC6ke diff --git a/crates/engram-protocol/tests/support/mod.rs b/crates/engram-protocol/tests/support/mod.rs index 770cced7d..0c51e3afb 100644 --- a/crates/engram-protocol/tests/support/mod.rs +++ b/crates/engram-protocol/tests/support/mod.rs @@ -329,6 +329,13 @@ pub fn session_egress_policy() -> impl Strategy { (secrets, injects, observes), (guest_services, secret_mode, apps), )| SessionEgressPolicy { + managed_deny: allow_all.then(|| { + engram_core::types::managed_deny::ManagedDenyPolicy::new( + (session % 1000) as u64, + ["mcp.example".into()], + ) + .unwrap() + }), session_id: SessionId::from(uuid::Uuid::from_u128(session)), sandbox_id: SandboxId::from(uuid::Uuid::from_u128(sandbox)), guest_ip: std::net::Ipv4Addr::from(ip), diff --git a/crates/engram-protocol/tests/wire_golden.rs b/crates/engram-protocol/tests/wire_golden.rs index 6a7fc0101..c078d5d33 100644 --- a/crates/engram-protocol/tests/wire_golden.rs +++ b/crates/engram-protocol/tests/wire_golden.rs @@ -222,6 +222,7 @@ fn snapshot_metadata() -> SnapshotMetadata { fn session_egress_policy() -> SessionEgressPolicy { SessionEgressPolicy { + managed_deny: None, session_id: SessionId::from(fixed_uuid(0x30)), sandbox_id: SandboxId::from(fixed_uuid(0x31)), guest_ip: Ipv4Addr::new(169, 254, 0, 21), @@ -261,6 +262,7 @@ fn session_egress_policy() -> SessionEgressPolicy { /// entry types can't silently drop off the corpus again. fn session_egress_policy_minted() -> SessionEgressPolicy { SessionEgressPolicy { + managed_deny: None, injects: vec![EgressInjectEntry { secret: "ghs_minted".into(), header_name: "authorization".into(), @@ -313,6 +315,7 @@ fn session_egress_policy_minted() -> SessionEgressPolicy { /// that #931 fixed stays pinned for a second provider. fn session_egress_policy_google() -> SessionEgressPolicy { SessionEgressPolicy { + managed_deny: None, network_allow_hosts: vec!["compute.googleapis.com".into()], injects: vec![EgressInjectEntry { secret: "ya29.minted".into(), @@ -354,6 +357,7 @@ fn session_egress_policy_google() -> SessionEgressPolicy { /// variant's encoding (index 1) inside a fully-populated entry. fn session_egress_policy_oauth() -> SessionEgressPolicy { SessionEgressPolicy { + managed_deny: None, injects: vec![EgressInjectEntry { secret: "linear-access-token".into(), header_name: "Authorization".into(), @@ -381,6 +385,7 @@ fn session_egress_policy_oauth() -> SessionEgressPolicy { /// fully-populated entry. fn session_egress_policy_oauth_user() -> SessionEgressPolicy { SessionEgressPolicy { + managed_deny: None, injects: vec![EgressInjectEntry { secret: "personal-access-token".into(), header_name: "Authorization".into(), @@ -729,8 +734,9 @@ fn wire_version_pinned() { // MigrationItem appends HotHint (bincode 3, protobuf 6). Existing item // indices stay fixed; the optional hint is separate from immutable disk seals. // Existing goldens embed neither payload and keep their bytes. + // 37 -> 38: SessionEgressPolicy appends the managed deny envelope. assert_eq!( - WIRE_VERSION, 37, + WIRE_VERSION, 38, "WIRE_VERSION changed — confirm payload goldens were regenerated too" ); } @@ -767,6 +773,7 @@ fn regen_golden() { }, ); write("session_egress_policy", &session_egress_policy()); + write("session_egress_policy_managed", &managed_egress_policy()); write( "session_egress_policy_minted", &session_egress_policy_minted(), @@ -908,3 +915,19 @@ fn warm_config_wire_shape_round_trips_only_when_stripped() { wire-strip in enabled_images.rs and this pin together" ); } + +fn managed_egress_policy() -> SessionEgressPolicy { + let mut policy = session_egress_policy(); + policy.managed_deny = Some( + engram_core::types::managed_deny::ManagedDenyPolicy::new( + 9, + ["mcp.example".into(), "alias.example".into()], + ) + .unwrap(), + ); + policy +} +#[test] +fn managed_policy_is_appended_to_egress_wire() { + assert_golden("session_egress_policy_managed", &managed_egress_policy()); +} diff --git a/crates/engram-sandbox-process/src/lib.rs b/crates/engram-sandbox-process/src/lib.rs index a103aa15f..c0e491db4 100644 --- a/crates/engram-sandbox-process/src/lib.rs +++ b/crates/engram-sandbox-process/src/lib.rs @@ -1973,6 +1973,7 @@ mod tests { let (backend, _dir) = backend(); let error = backend .notify_session_policy(SessionEgressPolicy { + managed_deny: None, session_id: SessionId::new(), sandbox_id: SandboxId::new(), guest_ip: std::net::Ipv4Addr::LOCALHOST, diff --git a/crates/engram-sim/src/meta/store_impl.rs b/crates/engram-sim/src/meta/store_impl.rs index 35228171c..9c99bdfe2 100644 --- a/crates/engram-sim/src/meta/store_impl.rs +++ b/crates/engram-sim/src/meta/store_impl.rs @@ -4293,15 +4293,72 @@ impl MetadataStore for SimMetadataStore { .unwrap_or_default()) } + async fn managed_policy_target( + &self, + session_id: SessionId, + ) -> Result { + self.gate()?; + let db = self.db.lock(); + let row = db.sessions.get(&session_id).ok_or(MetaError::NotFound)?; + if row.session.status.is_terminal() || row.binding_epoch <= 0 { + return Err(MetaError::NotFound); + } + Ok(engram_core::types::managed_deny::ManagedPolicyTarget { + session_id, + host_id: row.session.host_id.ok_or(MetaError::NotFound)?, + sandbox_id: row.session.sandbox_id.ok_or(MetaError::NotFound)?, + binding_epoch: row.binding_epoch as u64, + }) + } + async fn retain_managed_deny( + &self, + target: &engram_core::types::managed_deny::ManagedPolicyTarget, + policy: &engram_core::types::managed_deny::ManagedDenyPolicy, + ) -> Result<(), MetaError> { + self.gate()?; + let mut db = self.db.lock(); + let row = db + .sessions + .get(&target.session_id) + .ok_or(MetaError::NotFound)?; + if row.session.status.is_terminal() { + return Err(MetaError::NotFound); + } + if row.session.host_id != Some(target.host_id) + || row.session.sandbox_id != Some(target.sandbox_id) + || u64::try_from(row.binding_epoch).ok() != Some(target.binding_epoch) + { + return Err(MetaError::Conflict("managed placement changed".into())); + } + let next = engram_core::types::managed_deny::retain_in_policy_json( + db.session_integration_policy + .get(&target.session_id) + .map(String::as_str), + policy, + ) + .map_err(MetaError::Conflict)?; + db.session_integration_policy + .insert(target.session_id, next); + Ok(()) + } async fn bind_session_integration_policy( &self, session_id: SessionId, policy_json: &str, ) -> Result<(), MetaError> { self.gate()?; - self.db - .lock() - .session_integration_policy + let mut db = self.db.lock(); + if !db.sessions.contains_key(&session_id) { + return Err(MetaError::NotFound); + } + engram_core::types::managed_deny::validate_policy_replacement( + db.session_integration_policy + .get(&session_id) + .map(String::as_str), + policy_json, + ) + .map_err(MetaError::Conflict)?; + db.session_integration_policy .insert(session_id, policy_json.to_string()); Ok(()) } diff --git a/crates/engram-sim/tests/meta_conformance.rs b/crates/engram-sim/tests/meta_conformance.rs index 925ecbe84..61115b572 100644 --- a/crates/engram-sim/tests/meta_conformance.rs +++ b/crates/engram-sim/tests/meta_conformance.rs @@ -8811,3 +8811,61 @@ conformance!( t_mcp_read_execution_identity, super::mcp_read_execution_identity ); + +async fn managed_deny_retention(ctx: &Ctx) { + use engram_core::types::managed_deny::*; + let meta = &ctx.meta; + let sid = meta + .create_session(spec("test.invalid/managed:latest")) + .await + .unwrap(); + let hid = HostId::new(); + meta.upsert_host(host_record(hid, "managed-host", ctx.clock.now_utc())) + .await + .unwrap(); + meta.assign_session_host(sid, Some(hid)).await.unwrap(); + let sandbox = engram_core::SandboxId::new(); + let epoch = meta.transition_session_created(sid, sandbox).await.unwrap(); + let target = ManagedPolicyTarget { + session_id: sid, + sandbox_id: sandbox, + host_id: hid, + binding_epoch: epoch, + }; + assert_eq!(meta.managed_policy_target(sid).await.unwrap(), target); + let one = ManagedDenyPolicy::new(1, ["a.example".into()]).unwrap(); + meta.retain_managed_deny(&target, &one).await.unwrap(); + meta.retain_managed_deny(&target, &one).await.unwrap(); + assert!(meta + .bind_session_integration_policy(sid, "{}") + .await + .is_err()); + let smaller = ManagedDenyPolicy::new(2, ["b.example".into()]).unwrap(); + assert!(meta.retain_managed_deny(&target, &smaller).await.is_err()); + let union = ManagedDenyPolicy::new(2, ["a.example".into(), "b.example".into()]).unwrap(); + meta.retain_managed_deny(&target, &union).await.unwrap(); + assert!(meta.retain_managed_deny(&target, &one).await.is_err()); + let mut stale = target.clone(); + stale.binding_epoch += 1; + assert!(meta.retain_managed_deny(&stale, &union).await.is_err()); + let stored: engram_core::types::integration::IntegrationPolicy = serde_json::from_str( + &meta + .get_session_integration_policy(sid) + .await + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!(stored.managed_deny, Some(union.clone())); + assert_eq!( + meta.managed_policy_target(sid).await.unwrap(), + target, + "deny updates must not change physical epoch" + ); + meta.transition_session(sid, SessionState::Failed, BindingDisposition::Detach) + .await + .unwrap(); + assert!(meta.managed_policy_target(sid).await.is_err()); + assert!(meta.retain_managed_deny(&target, &union).await.is_err()); +} +conformance!(t_managed_deny_retention, super::managed_deny_retention); diff --git a/docs/adr/0125-isolated-external-mcp.md b/docs/adr/0125-isolated-external-mcp.md index 541b5127b..e671d8dfc 100644 --- a/docs/adr/0125-isolated-external-mcp.md +++ b/docs/adr/0125-isolated-external-mcp.md @@ -85,6 +85,50 @@ servers must authenticate writes. ### Network containment +Managed policy has a durable monotone host union. The coordinator saves the +desired union in the session integration policy before host I/O. The host saves +the desired state before proxy application, drains existing session connections, +then saves an applied marker before ACK. Read and compare-and-apply bind the exact +session, sandbox, physical epoch, host and proxy incarnations, binding hash, +operator identity, revision and digest. A lost ACK cannot remove retained hosts. +Managed host mode requires mTLS for the whole host mutation listener, exact +coordinator certificate pins, and host-specific server identity verification. +There is no simultaneous plaintext mutation listener or downgrade fallback. + +A managed host owns the durable policy directory with one lifetime exclusive OS +lock. The lock file keeps its inode and is never removed during handoff. The +lock releases on process exit or crash. A successor quarantines every managed +session reported by the surviving proxy before it reads durable records or +replays ordinary policies. Missing or corrupt records fail startup and keep +admission closed. Valid replay checks the physical target and live network +capability before it produces a new receipt for the successor incarnation. +Managed control requests use separate local proxy streams; a session drain does +not hold a global network lock. Directory ownership requires a persistent host +volume and one managed host generation per directory in deployment. + +Until the strict restore producer exists, known managed sessions reject restore, +base restore, resume, and agent start. The management marker is trusted host +state, and a corrupt marker does not make the session ordinary. A fresh target +host has no local marker: coordinator classification and pre-restore fencing are +still required before cross-host migration can be enabled. + +Fresh create and restore use a durable coordinator pre-start gate at the real +physical binding epoch. The orchestrator pulls preparation state, compiles and +commits a final candidate, then finalizes the exact manifest and host receipt. +The coordinator never holds a transaction waiting for the orchestrator. Initial +prompts remain durable and blocked until finalization. The native-only manifest +is an immutable creation input with authenticated session association. Restore +must quarantine guest execution and network before it can run saved state. + +The complete deny superset includes every operator-declared selectable authority +and alias in every zone, including unused zones. A durable operator generation +stages additions, closes new managed boot admission, and requires all affected +sessions to ACK the union or have a proved network fence before publication. +The worker configuration digest and deployment/network identity are separate +inputs. Missing capabilities and mixed generations stay closed. Network policy, +boot finalization, and operator publication are separate implementation slices; +the feature stays disabled until all producers and their composition are verified. + Accept canonical HTTPS URLs with exact approved authority and port (443 by default). Reject userinfo, query, fragment, IP literals, and redirects. A public zone denies private and reserved ranges. An internal zone allows only exact @@ -275,3 +319,25 @@ the implementation commit chain. - [Streamable HTTP 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) - [Official TypeScript SDK 2.4.0](https://github.com/modelcontextprotocol/typescript-sdk/releases/tag/v2.4.0) - [MCP authorization](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) + +### Managed network producer limits + +Managed sessions inspect otherwise allowed external TLS through the existing +HTTP interceptor. Literal mode does not add credentials. HTTP authority must +match the normalized TLS SNI and port. The proxy rejects opaque protocols, +HTTP CONNECT, upgrades, and chunked HTTP/1 requests that this adapter cannot +bound. Clients must trust the guest proxy CA; certificate-pinned clients and +custom non-HTTP TLS can fail. Ordinary sessions keep their current behavior. +Managed sessions cannot use connector-owned tunnels because the current +connector contract does not prove the external authority. The gateway and TLS +proxy share session connection cancellation and drain. + +A proxy acknowledgement alone does not prove complete guest enforcement. The +backend must supply a strict network proof tied to the exact host, sandbox, +physical epoch, guest IP, and network identity before a final binding ACK. +The default backend proof is absent. The existing Firecracker rules redirect +TCP 443 and DNS; they do not establish all-port enforcement. The next producer +slice must install pre-execution host controls for all TCP ports and close +unobserved UDP paths, including restore. Bootstrap receipts have no final +binding hash and cannot authorize tool exposure. Local socket tests do not +establish Firecracker, VZ, or harness compatibility acceptance. diff --git a/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts b/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts index e15af2eab..bc1be67ed 100644 --- a/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts +++ b/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts @@ -2,8 +2,8 @@ // @generated from file engram/mcp/v1/session_policy.proto (package engram.mcp.v1, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; -import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; import type { CreateSessionRequest, CreateSessionResponse, GetMcpWritePolicyResponse, McpPlanReview, McpTrustedWritePolicy, McpWritePolicyRef } from "../../app/v1/session_pb"; import { file_engram_app_v1_session } from "../../app/v1/session_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/session_policy.proto. */ export const file_engram_mcp_v1_session_policy: GenFile = /*@__PURE__*/ - fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCTK5BAoUU2Vzc2lvblBvbGljeVNlcnZpY2USbAoTQ3JlYXRlUG9saWN5U2Vzc2lvbhIpLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlcXVlc3QaKi5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRJgCg9SZWFkV3JpdGVQb2xpY3kSJS5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlcXVlc3QaJi5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlc3BvbnNlEl0KDkFjY2VwdFBsYW5FeGl0EiQuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlcXVlc3QaJS5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVzcG9uc2USfgoZVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eRIvLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlcXVlc3QaMC5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHlSZXNwb25zZRJyChVWYWxpZGF0ZURpc3BhdGNoRmVuY2USKy5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRGlzcGF0Y2hGZW5jZVJlcXVlc3QaLC5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRGlzcGF0Y2hGZW5jZVJlc3BvbnNlYgZwcm90bzM", [file_engram_app_v1_session]); + fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCSJlChNNYW5hZ2VkUG9saWN5VGFyZ2V0EhIKCnNlc3Npb25faWQYASABKAkSEgoKc2FuZGJveF9pZBgCIAEoCRIPCgdob3N0X2lkGAMgASgJEhUKDWJpbmRpbmdfZXBvY2gYBCABKAQi4AEKFE1hbmFnZWRQb2xpY3lDb250ZXh0EjIKBnRhcmdldBgBIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeVRhcmdldBIbChNvcGVyYXRvcl9nZW5lcmF0aW9uGAIgASgEEhkKEW9wZXJhdG9yX2lkZW50aXR5GAMgASgJEjAKBXBoYXNlGAQgASgOMiEuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5UGhhc2USGQoMYmluZGluZ19oYXNoGAUgASgJSACIAQFCDwoNX2JpbmRpbmdfaGFzaCJVChFNYW5hZ2VkRGVueVBvbGljeRIPCgd2ZXJzaW9uGAEgASgNEhAKCHJldmlzaW9uGAIgASgEEg0KBWhvc3RzGAMgAygJEg4KBmRpZ2VzdBgEIAEoCSKWAgoTTWFuYWdlZEFwcGxpZWRTdGF0ZRI0Cgdjb250ZXh0GAEgASgLMiMuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5Q29udGV4dBIwCgZwb2xpY3kYAiABKAsyIC5lbmdyYW0ubWNwLnYxLk1hbmFnZWREZW55UG9saWN5EhgKEGhvc3RfaW5jYXJuYXRpb24YAyABKAkSGQoRcHJveHlfaW5jYXJuYXRpb24YBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSPgoNbmV0d29ya19wcm9vZhgGIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZE5ldHdvcmtQcm9vZkgAiAEBQhAKDl9uZXR3b3JrX3Byb29mImAKGFJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIyCgZ0YXJnZXQYAiABKAsyIi5lbmdyYW0ubWNwLnYxLk1hbmFnZWRQb2xpY3lUYXJnZXQicwoZUmVhZE1hbmFnZWRQb2xpY3lSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRI4CgdhcHBsaWVkGAIgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkQXBwbGllZFN0YXRlSACIAQFCCgoIX2FwcGxpZWQi3QEKGUFwcGx5TWFuYWdlZFBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SNAoHY29udGV4dBgCIAEoCzIjLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeUNvbnRleHQSMAoGcG9saWN5GAMgASgLMiAuZW5ncmFtLm1jcC52MS5NYW5hZ2VkRGVueVBvbGljeRI5CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZEFwcGxpZWRTdGF0ZUgAiAEBQgsKCV9leHBlY3RlZCJjChpBcHBseU1hbmFnZWRQb2xpY3lSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIzCgdhcHBsaWVkGAIgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkQXBwbGllZFN0YXRlInUKE01hbmFnZWROZXR3b3JrUHJvb2YSMgoGdGFyZ2V0GAEgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5VGFyZ2V0EhAKCGd1ZXN0X2lwGAIgASgJEhgKEG5ldHdvcmtfaWRlbnRpdHkYAyABKAkqfgoSTWFuYWdlZFBvbGljeVBoYXNlEiQKIE1BTkFHRURfUE9MSUNZX1BIQVNFX1VOU1BFQ0lGSUVEEAASIgoeTUFOQUdFRF9QT0xJQ1lfUEhBU0VfQk9PVFNUUkFQEAESHgoaTUFOQUdFRF9QT0xJQ1lfUEhBU0VfRklOQUwQAjKMBgoUU2Vzc2lvblBvbGljeVNlcnZpY2USZgoRUmVhZE1hbmFnZWRQb2xpY3kSJy5lbmdyYW0ubWNwLnYxLlJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBooLmVuZ3JhbS5tY3AudjEuUmVhZE1hbmFnZWRQb2xpY3lSZXNwb25zZRJpChJBcHBseU1hbmFnZWRQb2xpY3kSKC5lbmdyYW0ubWNwLnYxLkFwcGx5TWFuYWdlZFBvbGljeVJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkFwcGx5TWFuYWdlZFBvbGljeVJlc3BvbnNlEmwKE0NyZWF0ZVBvbGljeVNlc3Npb24SKS5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DcmVhdGVQb2xpY3lTZXNzaW9uUmVzcG9uc2USYAoPUmVhZFdyaXRlUG9saWN5EiUuZW5ncmFtLm1jcC52MS5SZWFkV3JpdGVQb2xpY3lSZXF1ZXN0GiYuZW5ncmFtLm1jcC52MS5SZWFkV3JpdGVQb2xpY3lSZXNwb25zZRJdCg5BY2NlcHRQbGFuRXhpdBIkLmVuZ3JhbS5tY3AudjEuQWNjZXB0UGxhbkV4aXRSZXF1ZXN0GiUuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlc3BvbnNlEn4KGVZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHkSLy5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHlSZXF1ZXN0GjAuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVzcG9uc2UScgoVVmFsaWRhdGVEaXNwYXRjaEZlbmNlEisuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZWIGcHJvdG8z", [file_engram_app_v1_session]); /** * @generated from message engram.mcp.v1.CreatePolicySessionRequest @@ -319,12 +319,324 @@ export type ValidateExecutionIdentityResponse = Message<"engram.mcp.v1.ValidateE export const ValidateExecutionIdentityResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_session_policy, 9); +/** + * @generated from message engram.mcp.v1.ManagedPolicyTarget + */ +export type ManagedPolicyTarget = Message<"engram.mcp.v1.ManagedPolicyTarget"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string sandbox_id = 2; + */ + sandboxId: string; + + /** + * @generated from field: string host_id = 3; + */ + hostId: string; + + /** + * @generated from field: uint64 binding_epoch = 4; + */ + bindingEpoch: bigint; +}; + +/** + * Describes the message engram.mcp.v1.ManagedPolicyTarget. + * Use `create(ManagedPolicyTargetSchema)` to create a new message. + */ +export const ManagedPolicyTargetSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 10); + +/** + * @generated from message engram.mcp.v1.ManagedPolicyContext + */ +export type ManagedPolicyContext = Message<"engram.mcp.v1.ManagedPolicyContext"> & { + /** + * @generated from field: engram.mcp.v1.ManagedPolicyTarget target = 1; + */ + target?: ManagedPolicyTarget; + + /** + * @generated from field: uint64 operator_generation = 2; + */ + operatorGeneration: bigint; + + /** + * @generated from field: string operator_identity = 3; + */ + operatorIdentity: string; + + /** + * @generated from field: engram.mcp.v1.ManagedPolicyPhase phase = 4; + */ + phase: ManagedPolicyPhase; + + /** + * @generated from field: optional string binding_hash = 5; + */ + bindingHash?: string; +}; + +/** + * Describes the message engram.mcp.v1.ManagedPolicyContext. + * Use `create(ManagedPolicyContextSchema)` to create a new message. + */ +export const ManagedPolicyContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 11); + +/** + * @generated from message engram.mcp.v1.ManagedDenyPolicy + */ +export type ManagedDenyPolicy = Message<"engram.mcp.v1.ManagedDenyPolicy"> & { + /** + * @generated from field: uint32 version = 1; + */ + version: number; + + /** + * @generated from field: uint64 revision = 2; + */ + revision: bigint; + + /** + * @generated from field: repeated string hosts = 3; + */ + hosts: string[]; + + /** + * @generated from field: string digest = 4; + */ + digest: string; +}; + +/** + * Describes the message engram.mcp.v1.ManagedDenyPolicy. + * Use `create(ManagedDenyPolicySchema)` to create a new message. + */ +export const ManagedDenyPolicySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 12); + +/** + * @generated from message engram.mcp.v1.ManagedAppliedState + */ +export type ManagedAppliedState = Message<"engram.mcp.v1.ManagedAppliedState"> & { + /** + * @generated from field: engram.mcp.v1.ManagedPolicyContext context = 1; + */ + context?: ManagedPolicyContext; + + /** + * @generated from field: engram.mcp.v1.ManagedDenyPolicy policy = 2; + */ + policy?: ManagedDenyPolicy; + + /** + * @generated from field: string host_incarnation = 3; + */ + hostIncarnation: string; + + /** + * @generated from field: string proxy_incarnation = 4; + */ + proxyIncarnation: string; + + /** + * @generated from field: string state_id = 5; + */ + stateId: string; + + /** + * @generated from field: optional engram.mcp.v1.ManagedNetworkProof network_proof = 6; + */ + networkProof?: ManagedNetworkProof; +}; + +/** + * Describes the message engram.mcp.v1.ManagedAppliedState. + * Use `create(ManagedAppliedStateSchema)` to create a new message. + */ +export const ManagedAppliedStateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 13); + +/** + * @generated from message engram.mcp.v1.ReadManagedPolicyRequest + */ +export type ReadManagedPolicyRequest = Message<"engram.mcp.v1.ReadManagedPolicyRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ManagedPolicyTarget target = 2; + */ + target?: ManagedPolicyTarget; +}; + +/** + * Describes the message engram.mcp.v1.ReadManagedPolicyRequest. + * Use `create(ReadManagedPolicyRequestSchema)` to create a new message. + */ +export const ReadManagedPolicyRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 14); + +/** + * @generated from message engram.mcp.v1.ReadManagedPolicyResponse + */ +export type ReadManagedPolicyResponse = Message<"engram.mcp.v1.ReadManagedPolicyResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: optional engram.mcp.v1.ManagedAppliedState applied = 2; + */ + applied?: ManagedAppliedState; +}; + +/** + * Describes the message engram.mcp.v1.ReadManagedPolicyResponse. + * Use `create(ReadManagedPolicyResponseSchema)` to create a new message. + */ +export const ReadManagedPolicyResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 15); + +/** + * @generated from message engram.mcp.v1.ApplyManagedPolicyRequest + */ +export type ApplyManagedPolicyRequest = Message<"engram.mcp.v1.ApplyManagedPolicyRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ManagedPolicyContext context = 2; + */ + context?: ManagedPolicyContext; + + /** + * @generated from field: engram.mcp.v1.ManagedDenyPolicy policy = 3; + */ + policy?: ManagedDenyPolicy; + + /** + * @generated from field: optional engram.mcp.v1.ManagedAppliedState expected = 4; + */ + expected?: ManagedAppliedState; +}; + +/** + * Describes the message engram.mcp.v1.ApplyManagedPolicyRequest. + * Use `create(ApplyManagedPolicyRequestSchema)` to create a new message. + */ +export const ApplyManagedPolicyRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 16); + +/** + * @generated from message engram.mcp.v1.ApplyManagedPolicyResponse + */ +export type ApplyManagedPolicyResponse = Message<"engram.mcp.v1.ApplyManagedPolicyResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ManagedAppliedState applied = 2; + */ + applied?: ManagedAppliedState; +}; + +/** + * Describes the message engram.mcp.v1.ApplyManagedPolicyResponse. + * Use `create(ApplyManagedPolicyResponseSchema)` to create a new message. + */ +export const ApplyManagedPolicyResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 17); + +/** + * @generated from message engram.mcp.v1.ManagedNetworkProof + */ +export type ManagedNetworkProof = Message<"engram.mcp.v1.ManagedNetworkProof"> & { + /** + * @generated from field: engram.mcp.v1.ManagedPolicyTarget target = 1; + */ + target?: ManagedPolicyTarget; + + /** + * @generated from field: string guest_ip = 2; + */ + guestIp: string; + + /** + * @generated from field: string network_identity = 3; + */ + networkIdentity: string; +}; + +/** + * Describes the message engram.mcp.v1.ManagedNetworkProof. + * Use `create(ManagedNetworkProofSchema)` to create a new message. + */ +export const ManagedNetworkProofSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 18); + +/** + * These messages describe a host receipt, not permission to expose guest tools. + * + * @generated from enum engram.mcp.v1.ManagedPolicyPhase + */ +export enum ManagedPolicyPhase { + /** + * @generated from enum value: MANAGED_POLICY_PHASE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: MANAGED_POLICY_PHASE_BOOTSTRAP = 1; + */ + BOOTSTRAP = 1, + + /** + * @generated from enum value: MANAGED_POLICY_PHASE_FINAL = 2; + */ + FINAL = 2, +} + +/** + * Describes the enum engram.mcp.v1.ManagedPolicyPhase. + */ +export const ManagedPolicyPhaseSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_engram_mcp_v1_session_policy, 0); + /** * Dedicated pinned mTLS only. Create reuses the ordinary creation core. * * @generated from service engram.mcp.v1.SessionPolicyService */ export const SessionPolicyService: GenService<{ + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ReadManagedPolicy + */ + readManagedPolicy: { + methodKind: "unary"; + input: typeof ReadManagedPolicyRequestSchema; + output: typeof ReadManagedPolicyResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ApplyManagedPolicy + */ + applyManagedPolicy: { + methodKind: "unary"; + input: typeof ApplyManagedPolicyRequestSchema; + output: typeof ApplyManagedPolicyResponseSchema; + }, /** * @generated from rpc engram.mcp.v1.SessionPolicyService.CreatePolicySession */ diff --git a/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts b/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts index 2856683a0..98a61e86c 100644 --- a/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts +++ b/web/src/gen/engram/mcp/v1/session_policy-SessionPolicyService_connectquery.ts @@ -4,6 +4,16 @@ import { SessionPolicyService } from "./session_policy_pb"; +/** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ReadManagedPolicy + */ +export const readManagedPolicy = SessionPolicyService.method.readManagedPolicy; + +/** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ApplyManagedPolicy + */ +export const applyManagedPolicy = SessionPolicyService.method.applyManagedPolicy; + /** * @generated from rpc engram.mcp.v1.SessionPolicyService.CreatePolicySession */ diff --git a/web/src/gen/engram/mcp/v1/session_policy_pb.ts b/web/src/gen/engram/mcp/v1/session_policy_pb.ts index e15af2eab..bc1be67ed 100644 --- a/web/src/gen/engram/mcp/v1/session_policy_pb.ts +++ b/web/src/gen/engram/mcp/v1/session_policy_pb.ts @@ -2,8 +2,8 @@ // @generated from file engram/mcp/v1/session_policy.proto (package engram.mcp.v1, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; -import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; import type { CreateSessionRequest, CreateSessionResponse, GetMcpWritePolicyResponse, McpPlanReview, McpTrustedWritePolicy, McpWritePolicyRef } from "../../app/v1/session_pb"; import { file_engram_app_v1_session } from "../../app/v1/session_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/session_policy.proto. */ export const file_engram_mcp_v1_session_policy: GenFile = /*@__PURE__*/ - fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCTK5BAoUU2Vzc2lvblBvbGljeVNlcnZpY2USbAoTQ3JlYXRlUG9saWN5U2Vzc2lvbhIpLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlcXVlc3QaKi5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXNwb25zZRJgCg9SZWFkV3JpdGVQb2xpY3kSJS5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlcXVlc3QaJi5lbmdyYW0ubWNwLnYxLlJlYWRXcml0ZVBvbGljeVJlc3BvbnNlEl0KDkFjY2VwdFBsYW5FeGl0EiQuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlcXVlc3QaJS5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVzcG9uc2USfgoZVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eRIvLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlcXVlc3QaMC5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHlSZXNwb25zZRJyChVWYWxpZGF0ZURpc3BhdGNoRmVuY2USKy5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRGlzcGF0Y2hGZW5jZVJlcXVlc3QaLC5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRGlzcGF0Y2hGZW5jZVJlc3BvbnNlYgZwcm90bzM", [file_engram_app_v1_session]); + fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCSJlChNNYW5hZ2VkUG9saWN5VGFyZ2V0EhIKCnNlc3Npb25faWQYASABKAkSEgoKc2FuZGJveF9pZBgCIAEoCRIPCgdob3N0X2lkGAMgASgJEhUKDWJpbmRpbmdfZXBvY2gYBCABKAQi4AEKFE1hbmFnZWRQb2xpY3lDb250ZXh0EjIKBnRhcmdldBgBIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeVRhcmdldBIbChNvcGVyYXRvcl9nZW5lcmF0aW9uGAIgASgEEhkKEW9wZXJhdG9yX2lkZW50aXR5GAMgASgJEjAKBXBoYXNlGAQgASgOMiEuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5UGhhc2USGQoMYmluZGluZ19oYXNoGAUgASgJSACIAQFCDwoNX2JpbmRpbmdfaGFzaCJVChFNYW5hZ2VkRGVueVBvbGljeRIPCgd2ZXJzaW9uGAEgASgNEhAKCHJldmlzaW9uGAIgASgEEg0KBWhvc3RzGAMgAygJEg4KBmRpZ2VzdBgEIAEoCSKWAgoTTWFuYWdlZEFwcGxpZWRTdGF0ZRI0Cgdjb250ZXh0GAEgASgLMiMuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5Q29udGV4dBIwCgZwb2xpY3kYAiABKAsyIC5lbmdyYW0ubWNwLnYxLk1hbmFnZWREZW55UG9saWN5EhgKEGhvc3RfaW5jYXJuYXRpb24YAyABKAkSGQoRcHJveHlfaW5jYXJuYXRpb24YBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSPgoNbmV0d29ya19wcm9vZhgGIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZE5ldHdvcmtQcm9vZkgAiAEBQhAKDl9uZXR3b3JrX3Byb29mImAKGFJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIyCgZ0YXJnZXQYAiABKAsyIi5lbmdyYW0ubWNwLnYxLk1hbmFnZWRQb2xpY3lUYXJnZXQicwoZUmVhZE1hbmFnZWRQb2xpY3lSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRI4CgdhcHBsaWVkGAIgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkQXBwbGllZFN0YXRlSACIAQFCCgoIX2FwcGxpZWQi3QEKGUFwcGx5TWFuYWdlZFBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SNAoHY29udGV4dBgCIAEoCzIjLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeUNvbnRleHQSMAoGcG9saWN5GAMgASgLMiAuZW5ncmFtLm1jcC52MS5NYW5hZ2VkRGVueVBvbGljeRI5CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZEFwcGxpZWRTdGF0ZUgAiAEBQgsKCV9leHBlY3RlZCJjChpBcHBseU1hbmFnZWRQb2xpY3lSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIzCgdhcHBsaWVkGAIgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkQXBwbGllZFN0YXRlInUKE01hbmFnZWROZXR3b3JrUHJvb2YSMgoGdGFyZ2V0GAEgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5VGFyZ2V0EhAKCGd1ZXN0X2lwGAIgASgJEhgKEG5ldHdvcmtfaWRlbnRpdHkYAyABKAkqfgoSTWFuYWdlZFBvbGljeVBoYXNlEiQKIE1BTkFHRURfUE9MSUNZX1BIQVNFX1VOU1BFQ0lGSUVEEAASIgoeTUFOQUdFRF9QT0xJQ1lfUEhBU0VfQk9PVFNUUkFQEAESHgoaTUFOQUdFRF9QT0xJQ1lfUEhBU0VfRklOQUwQAjKMBgoUU2Vzc2lvblBvbGljeVNlcnZpY2USZgoRUmVhZE1hbmFnZWRQb2xpY3kSJy5lbmdyYW0ubWNwLnYxLlJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBooLmVuZ3JhbS5tY3AudjEuUmVhZE1hbmFnZWRQb2xpY3lSZXNwb25zZRJpChJBcHBseU1hbmFnZWRQb2xpY3kSKC5lbmdyYW0ubWNwLnYxLkFwcGx5TWFuYWdlZFBvbGljeVJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkFwcGx5TWFuYWdlZFBvbGljeVJlc3BvbnNlEmwKE0NyZWF0ZVBvbGljeVNlc3Npb24SKS5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DcmVhdGVQb2xpY3lTZXNzaW9uUmVzcG9uc2USYAoPUmVhZFdyaXRlUG9saWN5EiUuZW5ncmFtLm1jcC52MS5SZWFkV3JpdGVQb2xpY3lSZXF1ZXN0GiYuZW5ncmFtLm1jcC52MS5SZWFkV3JpdGVQb2xpY3lSZXNwb25zZRJdCg5BY2NlcHRQbGFuRXhpdBIkLmVuZ3JhbS5tY3AudjEuQWNjZXB0UGxhbkV4aXRSZXF1ZXN0GiUuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlc3BvbnNlEn4KGVZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHkSLy5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHlSZXF1ZXN0GjAuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVzcG9uc2UScgoVVmFsaWRhdGVEaXNwYXRjaEZlbmNlEisuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZWIGcHJvdG8z", [file_engram_app_v1_session]); /** * @generated from message engram.mcp.v1.CreatePolicySessionRequest @@ -319,12 +319,324 @@ export type ValidateExecutionIdentityResponse = Message<"engram.mcp.v1.ValidateE export const ValidateExecutionIdentityResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_session_policy, 9); +/** + * @generated from message engram.mcp.v1.ManagedPolicyTarget + */ +export type ManagedPolicyTarget = Message<"engram.mcp.v1.ManagedPolicyTarget"> & { + /** + * @generated from field: string session_id = 1; + */ + sessionId: string; + + /** + * @generated from field: string sandbox_id = 2; + */ + sandboxId: string; + + /** + * @generated from field: string host_id = 3; + */ + hostId: string; + + /** + * @generated from field: uint64 binding_epoch = 4; + */ + bindingEpoch: bigint; +}; + +/** + * Describes the message engram.mcp.v1.ManagedPolicyTarget. + * Use `create(ManagedPolicyTargetSchema)` to create a new message. + */ +export const ManagedPolicyTargetSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 10); + +/** + * @generated from message engram.mcp.v1.ManagedPolicyContext + */ +export type ManagedPolicyContext = Message<"engram.mcp.v1.ManagedPolicyContext"> & { + /** + * @generated from field: engram.mcp.v1.ManagedPolicyTarget target = 1; + */ + target?: ManagedPolicyTarget; + + /** + * @generated from field: uint64 operator_generation = 2; + */ + operatorGeneration: bigint; + + /** + * @generated from field: string operator_identity = 3; + */ + operatorIdentity: string; + + /** + * @generated from field: engram.mcp.v1.ManagedPolicyPhase phase = 4; + */ + phase: ManagedPolicyPhase; + + /** + * @generated from field: optional string binding_hash = 5; + */ + bindingHash?: string; +}; + +/** + * Describes the message engram.mcp.v1.ManagedPolicyContext. + * Use `create(ManagedPolicyContextSchema)` to create a new message. + */ +export const ManagedPolicyContextSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 11); + +/** + * @generated from message engram.mcp.v1.ManagedDenyPolicy + */ +export type ManagedDenyPolicy = Message<"engram.mcp.v1.ManagedDenyPolicy"> & { + /** + * @generated from field: uint32 version = 1; + */ + version: number; + + /** + * @generated from field: uint64 revision = 2; + */ + revision: bigint; + + /** + * @generated from field: repeated string hosts = 3; + */ + hosts: string[]; + + /** + * @generated from field: string digest = 4; + */ + digest: string; +}; + +/** + * Describes the message engram.mcp.v1.ManagedDenyPolicy. + * Use `create(ManagedDenyPolicySchema)` to create a new message. + */ +export const ManagedDenyPolicySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 12); + +/** + * @generated from message engram.mcp.v1.ManagedAppliedState + */ +export type ManagedAppliedState = Message<"engram.mcp.v1.ManagedAppliedState"> & { + /** + * @generated from field: engram.mcp.v1.ManagedPolicyContext context = 1; + */ + context?: ManagedPolicyContext; + + /** + * @generated from field: engram.mcp.v1.ManagedDenyPolicy policy = 2; + */ + policy?: ManagedDenyPolicy; + + /** + * @generated from field: string host_incarnation = 3; + */ + hostIncarnation: string; + + /** + * @generated from field: string proxy_incarnation = 4; + */ + proxyIncarnation: string; + + /** + * @generated from field: string state_id = 5; + */ + stateId: string; + + /** + * @generated from field: optional engram.mcp.v1.ManagedNetworkProof network_proof = 6; + */ + networkProof?: ManagedNetworkProof; +}; + +/** + * Describes the message engram.mcp.v1.ManagedAppliedState. + * Use `create(ManagedAppliedStateSchema)` to create a new message. + */ +export const ManagedAppliedStateSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 13); + +/** + * @generated from message engram.mcp.v1.ReadManagedPolicyRequest + */ +export type ReadManagedPolicyRequest = Message<"engram.mcp.v1.ReadManagedPolicyRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ManagedPolicyTarget target = 2; + */ + target?: ManagedPolicyTarget; +}; + +/** + * Describes the message engram.mcp.v1.ReadManagedPolicyRequest. + * Use `create(ReadManagedPolicyRequestSchema)` to create a new message. + */ +export const ReadManagedPolicyRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 14); + +/** + * @generated from message engram.mcp.v1.ReadManagedPolicyResponse + */ +export type ReadManagedPolicyResponse = Message<"engram.mcp.v1.ReadManagedPolicyResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: optional engram.mcp.v1.ManagedAppliedState applied = 2; + */ + applied?: ManagedAppliedState; +}; + +/** + * Describes the message engram.mcp.v1.ReadManagedPolicyResponse. + * Use `create(ReadManagedPolicyResponseSchema)` to create a new message. + */ +export const ReadManagedPolicyResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 15); + +/** + * @generated from message engram.mcp.v1.ApplyManagedPolicyRequest + */ +export type ApplyManagedPolicyRequest = Message<"engram.mcp.v1.ApplyManagedPolicyRequest"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ManagedPolicyContext context = 2; + */ + context?: ManagedPolicyContext; + + /** + * @generated from field: engram.mcp.v1.ManagedDenyPolicy policy = 3; + */ + policy?: ManagedDenyPolicy; + + /** + * @generated from field: optional engram.mcp.v1.ManagedAppliedState expected = 4; + */ + expected?: ManagedAppliedState; +}; + +/** + * Describes the message engram.mcp.v1.ApplyManagedPolicyRequest. + * Use `create(ApplyManagedPolicyRequestSchema)` to create a new message. + */ +export const ApplyManagedPolicyRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 16); + +/** + * @generated from message engram.mcp.v1.ApplyManagedPolicyResponse + */ +export type ApplyManagedPolicyResponse = Message<"engram.mcp.v1.ApplyManagedPolicyResponse"> & { + /** + * @generated from field: uint32 protocol = 1; + */ + protocol: number; + + /** + * @generated from field: engram.mcp.v1.ManagedAppliedState applied = 2; + */ + applied?: ManagedAppliedState; +}; + +/** + * Describes the message engram.mcp.v1.ApplyManagedPolicyResponse. + * Use `create(ApplyManagedPolicyResponseSchema)` to create a new message. + */ +export const ApplyManagedPolicyResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 17); + +/** + * @generated from message engram.mcp.v1.ManagedNetworkProof + */ +export type ManagedNetworkProof = Message<"engram.mcp.v1.ManagedNetworkProof"> & { + /** + * @generated from field: engram.mcp.v1.ManagedPolicyTarget target = 1; + */ + target?: ManagedPolicyTarget; + + /** + * @generated from field: string guest_ip = 2; + */ + guestIp: string; + + /** + * @generated from field: string network_identity = 3; + */ + networkIdentity: string; +}; + +/** + * Describes the message engram.mcp.v1.ManagedNetworkProof. + * Use `create(ManagedNetworkProofSchema)` to create a new message. + */ +export const ManagedNetworkProofSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 18); + +/** + * These messages describe a host receipt, not permission to expose guest tools. + * + * @generated from enum engram.mcp.v1.ManagedPolicyPhase + */ +export enum ManagedPolicyPhase { + /** + * @generated from enum value: MANAGED_POLICY_PHASE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: MANAGED_POLICY_PHASE_BOOTSTRAP = 1; + */ + BOOTSTRAP = 1, + + /** + * @generated from enum value: MANAGED_POLICY_PHASE_FINAL = 2; + */ + FINAL = 2, +} + +/** + * Describes the enum engram.mcp.v1.ManagedPolicyPhase. + */ +export const ManagedPolicyPhaseSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_engram_mcp_v1_session_policy, 0); + /** * Dedicated pinned mTLS only. Create reuses the ordinary creation core. * * @generated from service engram.mcp.v1.SessionPolicyService */ export const SessionPolicyService: GenService<{ + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ReadManagedPolicy + */ + readManagedPolicy: { + methodKind: "unary"; + input: typeof ReadManagedPolicyRequestSchema; + output: typeof ReadManagedPolicyResponseSchema; + }, + /** + * @generated from rpc engram.mcp.v1.SessionPolicyService.ApplyManagedPolicy + */ + applyManagedPolicy: { + methodKind: "unary"; + input: typeof ApplyManagedPolicyRequestSchema; + output: typeof ApplyManagedPolicyResponseSchema; + }, /** * @generated from rpc engram.mcp.v1.SessionPolicyService.CreatePolicySession */ From 647cef6dccd0c04973bad1c801e69dee68878943 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 09:20:35 -0500 Subject: [PATCH 030/279] Validate managed sandbox identity before agent start --- crates/engram-host-agent/src/bindings.rs | 38 +++- crates/engram-host-agent/src/grpc_server.rs | 7 +- crates/engram-host-agent/src/host_client.rs | 26 ++- crates/engram-host-agent/tests/managed_tls.rs | 192 +++++++++++++++++- 4 files changed, 247 insertions(+), 16 deletions(-) diff --git a/crates/engram-host-agent/src/bindings.rs b/crates/engram-host-agent/src/bindings.rs index 45c51cea3..08b60a092 100644 --- a/crates/engram-host-agent/src/bindings.rs +++ b/crates/engram-host-agent/src/bindings.rs @@ -117,6 +117,42 @@ impl BindingStore { .try_exists() .unwrap_or(true) } + /// Check every durable managed association, including a record whose binding + /// has been removed or moved. An unreadable association cannot prove ordinary. + pub(crate) fn managed_sandbox_required(&self, sandbox: SandboxId) -> std::io::Result { + let mut sessions = std::collections::BTreeSet::new(); + for entry in std::fs::read_dir(&self.dir)? { + let path = entry?.path(); + if matches!( + path.extension().and_then(|v| v.to_str()), + Some("managed" | "managed-required") + ) { + let sid: SessionId = path + .file_stem() + .and_then(|v| v.to_str()) + .ok_or_else(|| std::io::Error::other("invalid managed filename"))? + .parse() + .map_err(std::io::Error::other)?; + sessions.insert(sid); + } + } + for sid in sessions { + if self + .read(sid)? + .is_some_and(|binding| binding.sandbox_id == sandbox) + { + return Ok(true); + } + // The desired record can retain the old physical target after unbind. + let record = crate::managed_policy::read_record(self, sid)? + .ok_or_else(|| std::io::Error::other("managed record missing"))?; + if record.egress.sandbox_id == sandbox { + return Ok(true); + } + } + Ok(false) + } + pub(crate) fn directory(&self) -> &std::path::Path { &self.dir } @@ -192,7 +228,7 @@ impl BindingStore { format!("binding record {}: {e}", path.display()), ) })?; - if record.schema_version != BINDING_SCHEMA_VERSION { + if record.schema_version != BINDING_SCHEMA_VERSION || record.session_id != session_id { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, format!( diff --git a/crates/engram-host-agent/src/grpc_server.rs b/crates/engram-host-agent/src/grpc_server.rs index 68e42dd81..1959b2322 100644 --- a/crates/engram-host-agent/src/grpc_server.rs +++ b/crates/engram-host-agent/src/grpc_server.rs @@ -1132,7 +1132,12 @@ impl HostService for HostServiceImpl { check_wire_version(&req)?; async move { let r = req.into_inner(); - let fence = self.check_session_epoch(&r.session_id, r.fencing_epoch)?; + let mut fence = self.check_session_epoch(&r.session_id, r.fencing_epoch)?; + // Epoch zero skips high-water updates, but must not erase a supplied + // identity before the start gate compares it with the policy. + if r.fencing_epoch == 0 && !r.session_id.is_empty() { + fence.session_id = decode_session_id(&r.session_id)?; + } let sandbox_id = decode_sandbox_id(&r.sandbox_id)?; let agent = decode_bincode(&r.agent_bincode, "AgentSpec")?; let policy = decode_bincode(&r.policy_bincode, "SessionEgressPolicy")?; diff --git a/crates/engram-host-agent/src/host_client.rs b/crates/engram-host-agent/src/host_client.rs index 910b28f85..4d1010ea9 100644 --- a/crates/engram-host-agent/src/host_client.rs +++ b/crates/engram-host-agent/src/host_client.rs @@ -66,12 +66,9 @@ impl LocalHostClient { let bindings = self.harness_hub.binding_store(); let mut managed = bindings.managed_required(fence.session_id); if let Some(sandbox) = sandbox { - if let Some(binding) = bindings - .find_by_sandbox(sandbox) - .map_err(|e| SandboxError::InvalidSpec(e.to_string()))? - { - managed |= bindings.managed_required(binding.session_id); - } + managed |= bindings + .managed_sandbox_required(sandbox) + .map_err(|e| SandboxError::InvalidSpec(e.to_string()))?; } if managed { return Err(SandboxError::InvalidSpec( @@ -357,9 +354,14 @@ impl HostClient for LocalHostClient { id: SandboxId, agent: AgentSpec, policy: SessionEgressPolicy, - _fence: SessionFence, + fence: SessionFence, ) -> Result<(), SandboxError> { - if policy.managed_deny.is_some() + let bindings = self.harness_hub.binding_store(); + if bindings + .managed_sandbox_required(id) + .map_err(|e| SandboxError::InvalidSpec(e.to_string()))? + || bindings.managed_required(fence.session_id) + || policy.managed_deny.is_some() || self .harness_hub .binding_store() @@ -369,6 +371,14 @@ impl HostClient for LocalHostClient { "managed session requires acknowledged boot finalization".into(), )); } + // Legacy ordinary callers can still use the explicit unfenced sentinel. + // Every supplied identity must otherwise agree before any durable mutation. + if policy.sandbox_id != id + || policy.session_id.as_uuid().is_nil() + || (fence != SessionFence::unfenced() && fence.session_id != policy.session_id) + { + return Err(SandboxError::InvalidSpec("start identity mismatch".into())); + } // ADR 0073: persist the binding record BEFORE the harness can // dial (the spawn below), so the first attach validates instead // of bouncing UnknownBinding. Monotonic: a stale caller's bind diff --git a/crates/engram-host-agent/tests/managed_tls.rs b/crates/engram-host-agent/tests/managed_tls.rs index e3b33798f..bc1246d19 100644 --- a/crates/engram-host-agent/tests/managed_tls.rs +++ b/crates/engram-host-agent/tests/managed_tls.rs @@ -123,7 +123,10 @@ async fn managed_host_requires_pinned_mutual_tls() { ca.cert_pem, None, )); - let backend = Arc::new(EndpointFixture(egress.clone())); + let backend = Arc::new(EndpointFixture { + egress: egress.clone(), + calls: Default::default(), + }); let bindings = engram_host_agent::bindings::BindingStore::open(dir.join("bindings")).unwrap(); let target = ManagedPolicyTarget { session_id: engram_core::SessionId::new(), @@ -151,10 +154,10 @@ async fn managed_host_requires_pinned_mutual_tls() { ); let hub = Arc::new(engram_host_agent::harness::HarnessHub::new( engram_host_agent::harness::event_sink_to(|_, _, _, _| async { Ok(()) }), - bindings, + bindings.clone(), )); let local = Arc::new( - engram_host_agent::host_client::LocalHostClient::new(backend, hub) + engram_host_agent::host_client::LocalHostClient::new(backend.clone(), hub) .with_managed_policy(managed), ); let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); @@ -243,7 +246,7 @@ async fn managed_host_requires_pinned_mutual_tls() { let apply = ManagedPolicyApply { context, expected: None, - egress: policy, + egress: policy.clone(), }; let ack = remote.apply_managed_policy(apply.clone()).await.unwrap(); assert_eq!( @@ -255,6 +258,151 @@ async fn managed_host_requires_pinned_mutual_tls() { ack.context.binding_hash.is_none() && ack.network_proof.is_none(), "bootstrap is not final network proof" ); + // Every generic start must validate durable sandbox identity before bind or policy I/O. + // An ordinary duplicate binding must not hide the managed association. + let alias = engram_core::SessionId::new(); + bindings.bind(alias, target.sandbox_id, 1).unwrap(); + let agent = engram_core::types::sandbox::AgentSpec { + binding_epoch: 1, + argv: vec!["fixture".into()], + env: Default::default(), + session_env: Default::default(), + host_ca_pem: None, + }; + let binding_bytes = || { + std::fs::read_dir(dir.join("bindings")) + .unwrap() + .map(|entry| { + let path = entry.unwrap().path(); + ( + path.file_name().unwrap().to_owned(), + std::fs::read(path).unwrap(), + ) + }) + .collect::>() + }; + for host in [&*local as &dyn HostClient, &remote as &dyn HostClient] { + let mut ordinary = policy.clone(); + ordinary.managed_deny = None; + ordinary.session_id = engram_core::SessionId::new(); + let fence = engram_core::traits::SessionFence::new(ordinary.session_id, 1); + let before = binding_bytes(); + let calls = backend.calls.load(std::sync::atomic::Ordering::SeqCst); + for supplied_fence in [fence, engram_core::traits::SessionFence::unfenced()] { + let error = host + .start_agent( + target.sandbox_id, + agent.clone(), + ordinary.clone(), + supplied_fence, + ) + .await + .unwrap_err(); + assert!( + error + .to_string() + .contains("managed session requires acknowledged boot finalization"), + "{error}" + ); + } + assert_eq!( + binding_bytes(), + before, + "rejected start must not bind or persist policy" + ); + assert_eq!( + backend.calls.load(std::sync::atomic::Ordering::SeqCst), + calls + ); + assert_eq!( + host.read_managed_policy(target.clone()).await.unwrap(), + Some(ack.clone()) + ); + ordinary.sandbox_id = engram_core::SandboxId::new(); + for (sandbox, supplied_fence) in [ + (engram_core::SandboxId::new(), fence), + ( + ordinary.sandbox_id, + engram_core::traits::SessionFence::new(engram_core::SessionId::new(), 1), + ), + ( + ordinary.sandbox_id, + engram_core::traits::SessionFence::new(engram_core::SessionId::new(), 0), + ), + ] { + let error = host + .start_agent(sandbox, agent.clone(), ordinary.clone(), supplied_fence) + .await + .unwrap_err(); + assert!( + error.to_string().contains("start identity mismatch"), + "{error}" + ); + } + assert_eq!(binding_bytes(), before); + assert_eq!( + backend.calls.load(std::sync::atomic::Ordering::SeqCst), + calls + ); + ordinary.guest_ip = "10.200.0.43".parse().unwrap(); + host.start_agent(ordinary.sandbox_id, agent.clone(), ordinary.clone(), fence) + .await + .unwrap(); + assert_eq!( + backend.calls.load(std::sync::atomic::Ordering::SeqCst), + calls + 2 + ); + assert_eq!( + bindings + .read(ordinary.session_id) + .unwrap() + .unwrap() + .sandbox_id, + ordinary.sandbox_id + ); + host.start_agent( + ordinary.sandbox_id, + agent.clone(), + ordinary, + engram_core::traits::SessionFence::unfenced(), + ) + .await + .unwrap(); + assert_eq!( + backend.calls.load(std::sync::atomic::Ordering::SeqCst), + calls + 4 + ); + } + // The retained managed record still names the sandbox after the binding is removed. + let binding_path = dir + .join("bindings") + .join(format!("{}.json", target.session_id)); + let binding = std::fs::read(&binding_path).unwrap(); + std::fs::remove_file(&binding_path).unwrap(); + let mut disguised = policy.clone(); + disguised.managed_deny = None; + disguised.session_id = engram_core::SessionId::new(); + for host in [&*local as &dyn HostClient, &remote as &dyn HostClient] { + let before = binding_bytes(); + let calls = backend.calls.load(std::sync::atomic::Ordering::SeqCst); + assert!(host + .start_agent( + target.sandbox_id, + agent.clone(), + disguised.clone(), + engram_core::traits::SessionFence::unfenced() + ) + .await + .unwrap_err() + .to_string() + .contains("managed session requires acknowledged boot finalization")); + assert_eq!(binding_bytes(), before); + assert_eq!( + backend.calls.load(std::sync::atomic::Ordering::SeqCst), + calls + ); + } + std::fs::write(binding_path, binding).unwrap(); let snapshot: engram_core::types::snapshot::SnapshotMetadata = serde_json::from_value(serde_json::json!({ "id": engram_core::SnapshotId::new(), "size_bytes": 0, @@ -318,6 +466,26 @@ async fn managed_host_requires_pinned_mutual_tls() { .unwrap_err() .to_string() .contains("managed restore quarantine unavailable")); + for host in [&*local as &dyn HostClient, &remote as &dyn HostClient] { + let before = binding_bytes(); + let calls = backend.calls.load(std::sync::atomic::Ordering::SeqCst); + assert!(host + .start_agent( + target.sandbox_id, + agent.clone(), + disguised.clone(), + engram_core::traits::SessionFence::unfenced() + ) + .await + .unwrap_err() + .to_string() + .contains("managed session requires acknowledged boot finalization")); + assert_eq!(binding_bytes(), before); + assert_eq!( + backend.calls.load(std::sync::atomic::Ordering::SeqCst), + calls + ); + } client.client_certificate = dir.join("other.pem"); client.client_key = dir.join("other.key"); assert!( @@ -379,9 +547,20 @@ async fn managed_host_requires_pinned_mutual_tls() { let _ = server.await; } -struct EndpointFixture(Arc); +struct EndpointFixture { + egress: Arc, + calls: std::sync::atomic::AtomicUsize, +} #[async_trait::async_trait] impl engram_core::traits::SandboxBackend for EndpointFixture { + async fn start_agent( + &self, + _: engram_core::SandboxId, + _: engram_core::types::sandbox::AgentSpec, + ) -> Result<(), engram_core::SandboxError> { + self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Ok(()) + } async fn create( &self, _: engram_core::types::sandbox::SandboxSpec, @@ -442,7 +621,8 @@ impl engram_core::traits::SandboxBackend for EndpointFixture { &self, policy: engram_core::types::egress::SessionEgressPolicy, ) -> Result<(), engram_core::SandboxError> { - self.0 + self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + self.egress .apply_policy(policy) .await .map_err(|e| engram_core::SandboxError::InvalidSpec(e.to_string())) From c46c4fef794b2b2f28909424f51f5e9c03efd3b4 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 09:20:47 -0500 Subject: [PATCH 031/279] Expose retained desired policies in trusted reads --- crates/engram-coordinator/src/mcp/policy.rs | 168 ++++---- .../src/mcp/policy_tests.rs | 377 ++++++++++++++++++ .../proto/engram/mcp/v1/session_policy.proto | 8 + crates/engram-protocol/src/managed_policy.rs | 26 ++ .../engram-protocol/tests/mcp_policy_wire.rs | 65 +++ docs/adr/0125-isolated-external-mcp.md | 15 + .../gen/engram/mcp/v1/session_policy_pb.ts | 41 +- .../gen/engram/mcp/v1/session_policy_pb.ts | 41 +- 8 files changed, 663 insertions(+), 78 deletions(-) create mode 100644 crates/engram-coordinator/src/mcp/policy_tests.rs diff --git a/crates/engram-coordinator/src/mcp/policy.rs b/crates/engram-coordinator/src/mcp/policy.rs index 560a19135..57b5d48f0 100644 --- a/crates/engram-coordinator/src/mcp/policy.rs +++ b/crates/engram-coordinator/src/mcp/policy.rs @@ -124,24 +124,7 @@ impl SessionPolicyService for PolicyControl { .state .as_ref() .ok_or_else(|| Status::unavailable("managed policy unavailable"))?; - check_managed_target(state, &target).await?; - let applied = state - .services - .host - .read_managed_policy(target.clone()) - .await - .map_err(|_| Status::failed_precondition("host policy unavailable"))?; - check_managed_target(state, &target).await?; - if let Some(receipt) = &applied { - receipt.validate().map_err(Status::failed_precondition)?; - if receipt.context.target != target { - return Err(Status::failed_precondition("host policy identity changed")); - } - } - Ok(Response::new(wire::ReadManagedPolicyResponse { - protocol: wire::PROTOCOL, - applied: applied.map(Into::into), - })) + read_managed_policy(state, &target).await } async fn apply_managed_policy( &self, @@ -168,58 +151,7 @@ impl SessionPolicyService for PolicyControl { .state .as_ref() .ok_or_else(|| Status::unavailable("managed policy unavailable"))?; - let target = &context.target; - check_managed_target(state, target).await?; - // The desired union commits before any host call. A lost ACK cannot discard it on migration. - state - .services - .meta - .retain_managed_deny(target, &policy) - .await - .map_err(err)?; - let session = state - .services - .meta - .get_session(target.session_id) - .await - .map_err(err)?; - let egress = crate::api::sessions::build_resume_egress_policy( - state, - target.session_id, - target.sandbox_id, - &session.image.to_string(), - ) - .await - .map_err(|_| Status::failed_precondition("managed egress source unavailable"))? - .ok_or_else(|| Status::failed_precondition("managed guest identity unavailable"))?; - if !egress.tunnels.is_empty() { - return Err(Status::failed_precondition( - "managed policy does not support tunnels", - )); - } - if egress.managed_deny.as_ref() != Some(&policy) { - return Err(Status::failed_precondition("desired policy changed")); - } - check_managed_target(state, target).await?; - let applied = state - .services - .host - .apply_managed_policy(engram_core::types::managed_deny::ManagedPolicyApply { - expected, - context: context.clone(), - egress, - }) - .await - .map_err(|_| Status::failed_precondition("host policy unavailable"))?; - check_managed_target(state, target).await?; - applied.validate().map_err(Status::failed_precondition)?; - if applied.context != context || applied.policy != policy { - return Err(Status::failed_precondition("host acknowledgement mismatch")); - } - Ok(Response::new(wire::ApplyManagedPolicyResponse { - protocol: wire::PROTOCOL, - applied: Some(applied.into()), - })) + apply_managed_policy(state, context, policy, expected).await } async fn create_policy_session( &self, @@ -422,6 +354,98 @@ impl SessionPolicyService for PolicyControl { } } +async fn read_managed_policy( + state: &SharedState, + target: &engram_core::types::managed_deny::ManagedPolicyTarget, +) -> Result, Status> { + check_managed_target(state, target).await?; + let applied = state + .services + .host + .read_managed_policy(target.clone()) + .await + .map_err(|_| Status::failed_precondition("host policy unavailable"))?; + // Read after host I/O so a concurrent desired commit is retained. No SQL + // transaction crosses host I/O, and persisted bytes never become an ACK. + let retained = crate::api::sessions::load_session_policy(state, target.session_id) + .await + .map_err(|_| Status::failed_precondition("retained policy unavailable"))? + .and_then(|policy| policy.managed_deny); + check_managed_target(state, target).await?; + let response = wire::ReadManagedPolicyResponse { + protocol: wire::PROTOCOL, + applied: applied.map(Into::into), + retained_desired: Some(wire::ManagedRetainedDesired { + target: Some(target.clone().into()), + policy: retained.map(Into::into), + }), + }; + response + .validate_for(target) + .map_err(Status::failed_precondition)?; + Ok(Response::new(response)) +} + +async fn apply_managed_policy( + state: &SharedState, + context: engram_core::types::managed_deny::ManagedPolicyContext, + policy: engram_core::types::managed_deny::ManagedDenyPolicy, + expected: Option, +) -> Result, Status> { + let target = &context.target; + check_managed_target(state, target).await?; + // The desired union commits before any host call. A lost ACK cannot discard it on migration. + state + .services + .meta + .retain_managed_deny(target, &policy) + .await + .map_err(err)?; + let session = state + .services + .meta + .get_session(target.session_id) + .await + .map_err(err)?; + let egress = crate::api::sessions::build_resume_egress_policy( + state, + target.session_id, + target.sandbox_id, + &session.image.to_string(), + ) + .await + .map_err(|_| Status::failed_precondition("managed egress source unavailable"))? + .ok_or_else(|| Status::failed_precondition("managed guest identity unavailable"))?; + if !egress.tunnels.is_empty() { + return Err(Status::failed_precondition( + "managed policy does not support tunnels", + )); + } + if egress.managed_deny.as_ref() != Some(&policy) { + return Err(Status::failed_precondition("desired policy changed")); + } + check_managed_target(state, target).await?; + let applied = state + .services + .host + .apply_managed_policy(engram_core::types::managed_deny::ManagedPolicyApply { + expected, + context: context.clone(), + egress, + }) + .await + .map_err(|_| Status::failed_precondition("host policy unavailable"))?; + check_managed_target(state, target).await?; + applied.validate().map_err(Status::failed_precondition)?; + if applied.context != context || applied.policy != policy { + return Err(Status::failed_precondition("host acknowledgement mismatch")); + } + Ok(Response::new(wire::ApplyManagedPolicyResponse { + protocol: wire::PROTOCOL, + applied: Some(applied.into()), + })) +} + async fn check_managed_target( state: &SharedState, target: &engram_core::types::managed_deny::ManagedPolicyTarget, @@ -466,3 +490,7 @@ mod tests { assert!(creation(&r).is_err()); } } + +#[cfg(test)] +#[path = "policy_tests.rs"] +mod recovery_tests; diff --git a/crates/engram-coordinator/src/mcp/policy_tests.rs b/crates/engram-coordinator/src/mcp/policy_tests.rs new file mode 100644 index 000000000..9568bae2d --- /dev/null +++ b/crates/engram-coordinator/src/mcp/policy_tests.rs @@ -0,0 +1,377 @@ +//! Coordinator recovery through the actual apply/read functions; no guest is started. +use super::*; +use async_trait::async_trait; +use engram_core::traits::{Clock, Entropy, HostClient, MetadataStore, SessionFence}; +use engram_core::types::egress::SessionEgressPolicy; +use engram_core::types::managed_deny::*; +use engram_core::types::sandbox::{ + AgentSpec, ExecRequest as HostExecRequest, ExecStream, SandboxProbe, SandboxSpec, +}; +use engram_core::types::snapshot::SnapshotMetadata; +use engram_core::types::{host::*, SessionSpec}; +use engram_core::{SandboxError, SandboxId}; +use engram_sim::{ManualClock, MemBlobStorage, SimEntropy, SimMetadataStore}; +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; + +struct PolicyHost { + applied: Mutex>, + entered: tokio::sync::Notify, + release: tokio::sync::Semaphore, +} +#[async_trait] +impl HostClient for PolicyHost { + async fn read_managed_policy( + &self, + _: ManagedPolicyTarget, + ) -> Result, SandboxError> { + Ok(self.applied.lock().unwrap().clone()) + } + async fn apply_managed_policy( + &self, + request: ManagedPolicyApply, + ) -> Result { + let mut current = self.applied.lock().unwrap(); + assert_eq!( + *current, request.expected, + "exact applied receipt must remain the CAS input" + ); + let ack = ManagedAppliedState::new( + request.context, + request.egress.managed_deny.unwrap(), + "fixture-host".into(), + "fixture-proxy".into(), + None, + ) + .unwrap(); + *current = Some(ack.clone()); + Ok(ack) + } + async fn create(&self, _spec: SandboxSpec) -> Result { + unreachable!("exec-core tests never create sandboxes") + } + + async fn destroy(&self, _id: SandboxId, _fence: SessionFence) -> Result<(), SandboxError> { + unreachable!("exec-core tests never destroy sandboxes") + } + + async fn list(&self) -> Result, SandboxError> { + unreachable!("exec-core tests never list sandboxes") + } + + async fn probe_sandbox(&self, _id: SandboxId) -> Result { + unreachable!("exec-core tests never probe sandboxes") + } + + async fn exec_stream( + &self, + id: SandboxId, + cmd: HostExecRequest, + ) -> Result { + let _ = (id, cmd); + unreachable!("readiness never executes commands") + } + + async fn snapshot( + &self, + _id: SandboxId, + _fence: SessionFence, + ) -> Result { + unreachable!("exec-core tests never snapshot sandboxes") + } + + async fn restore( + &self, + _metadata: SnapshotMetadata, + _fence: SessionFence, + ) -> Result { + unreachable!("exec-core tests never restore sandboxes") + } + + async fn start_agent( + &self, + _id: SandboxId, + _agent: AgentSpec, + _policy: SessionEgressPolicy, + _fence: SessionFence, + ) -> Result<(), SandboxError> { + unreachable!("policy control never starts guests") + } + + async fn guest_ip(&self, _id: SandboxId) -> Option { + self.entered.notify_one(); + self.release.acquire().await.unwrap().forget(); + Some("10.200.0.42".parse().unwrap()) + } + + async fn bind_session( + &self, + _session_id: SessionId, + _sandbox_id: SandboxId, + _binding_epoch: u64, + ) -> Result<(), engram_core::SandboxError> { + Err(SandboxError::Vm("bind rejected".into())) + } + + async fn unbind_session(&self, _session_id: SessionId) {} + + async fn send_prompt( + &self, + _sandbox_id: SandboxId, + _prompt_id: String, + _text: String, + _mode: Option, + ) -> Result<(), SandboxError> { + unreachable!("exec-core tests never send prompts") + } +} + +async fn retained_recovery(meta: Arc, clock: Arc) { + let entropy = Arc::new(SimEntropy::seeded(111)); + let sid = meta + .create_session(SessionSpec { + image: "test.invalid/policy:latest".into(), + mode: Default::default(), + }) + .await + .unwrap(); + let host_id = engram_core::HostId::from(entropy.uuid()); + let sandbox = SandboxId::from(entropy.uuid()); + meta.upsert_host(host_record(host_id, "fixture-host", clock.now_utc())) + .await + .unwrap(); + meta.assign_session_host(sid, Some(host_id)).await.unwrap(); + let binding_epoch = meta.transition_session_created(sid, sandbox).await.unwrap(); + let target = ManagedPolicyTarget { + session_id: sid, + sandbox_id: sandbox, + host_id, + binding_epoch, + }; + let context = ManagedPolicyContext { + target: target.clone(), + operator_generation: 1, + operator_identity: format!("sha256:{}", "a".repeat(64)), + binding_hash: None, + }; + let one = ManagedDenyPolicy::new(1, ["a.example".into()]).unwrap(); + meta.retain_managed_deny(&target, &one).await.unwrap(); + let initial_ack = ManagedAppliedState::new( + context.clone(), + one.clone(), + "fixture-host".into(), + "fixture-proxy".into(), + None, + ) + .unwrap(); + let host = Arc::new(PolicyHost { + applied: Mutex::new(Some(initial_ack.clone())), + entered: Default::default(), + release: tokio::sync::Semaphore::new(0), + }); + let blob = Arc::new(MemBlobStorage::new()); + let services = crate::Services { + meta: meta.clone(), + host: host.clone(), + host_pool: Arc::new(engram_protocol::grpc_pool::GrpcHostPool::new()), + secrets: Arc::new(engram_secrets_dev::InMemorySecretStore::new()), + kek: Arc::new(engram_crypto::EnvVarKeyProvider::from_bytes( + [0u8; 32], "test:v1", + )), + oci: Arc::new(engram_oci::OciClient::new(Arc::new( + engram_oci::AnonymousResolver, + ))), + auth_resolver: Arc::new(engram_oci::AnonymousResolver), + blob: blob.clone(), + chunk_store: engram_chunk_store::ChunkStore::new(blob), + materialize_dir: None, + clock, + entropy, + }; + let state = Arc::new(crate::AppState::new( + crate::CoordinatorConfig::default(), + services, + )); + // Explicit wrapper + absent policy is authoritative absence for a fresh row. + let fresh = meta + .create_session(SessionSpec { + image: "test.invalid/fresh:latest".into(), + mode: Default::default(), + }) + .await + .unwrap(); + meta.assign_session_host(fresh, Some(host_id)) + .await + .unwrap(); + let fresh_sandbox = SandboxId::from(state.services.entropy.uuid()); + let fresh_epoch = meta + .transition_session_created(fresh, fresh_sandbox) + .await + .unwrap(); + let fresh_target = ManagedPolicyTarget { + session_id: fresh, + sandbox_id: fresh_sandbox, + host_id, + binding_epoch: fresh_epoch, + }; + *host.applied.lock().unwrap() = None; + let empty = read_managed_policy(&state, &fresh_target) + .await + .unwrap() + .into_inner(); + empty.validate_for(&fresh_target).unwrap(); + assert!(empty.applied.is_none()); + assert!(empty.retained_desired.unwrap().policy.is_none()); + *host.applied.lock().unwrap() = Some(initial_ack.clone()); + let three = ManagedDenyPolicy::new(3, ["a.example".into(), "orphan.example".into()]).unwrap(); + let pending = tokio::spawn({ + let state = state.clone(); + let context = context.clone(); + let three = three.clone(); + let expected = initial_ack.clone(); + async move { apply_managed_policy(&state, context, three, Some(expected)).await } + }); + tokio::time::timeout(Duration::from_secs(3), host.entered.notified()) + .await + .unwrap(); + // The real apply path is suspended at guest-IP host I/O after desired commit. + // A concurrent writer on the same row must complete while that I/O is held. + tokio::time::timeout( + Duration::from_secs(3), + meta.retain_managed_deny(&target, &three), + ) + .await + .unwrap() + .unwrap(); + assert_eq!(*host.applied.lock().unwrap(), Some(initial_ack.clone())); + pending.abort(); + assert!(pending.await.unwrap_err().is_cancelled()); + for applied in [Some(initial_ack.clone()), None] { + *host.applied.lock().unwrap() = applied.clone(); + let response = read_managed_policy(&state, &target) + .await + .unwrap() + .into_inner(); + assert_eq!( + response + .applied + .map(ManagedAppliedState::try_from) + .transpose() + .unwrap(), + applied + ); + let retained = response + .retained_desired + .expect("complete read must expose the committed orphan union"); + assert_eq!( + ManagedPolicyTarget::try_from(retained.target.unwrap()).unwrap(), + target + ); + assert_eq!( + ManagedDenyPolicy::try_from(retained.policy.unwrap()).unwrap(), + three + ); + } + // A fresh consumer chooses max(retained, applied) + 1 and preserves both unions. + let read = read_managed_policy(&state, &target) + .await + .unwrap() + .into_inner(); + let retained: ManagedDenyPolicy = read + .retained_desired + .unwrap() + .policy + .unwrap() + .try_into() + .unwrap(); + let expected = read + .applied + .map(ManagedAppliedState::try_from) + .transpose() + .unwrap(); + let revision = retained + .revision + .max(expected.as_ref().map_or(0, |ack| ack.policy.revision)) + + 1; + let four = ManagedDenyPolicy::new( + revision, + retained.hosts.into_iter().chain(["next.example".into()]), + ) + .unwrap(); + host.release.add_permits(1); + let retry = apply_managed_policy(&state, context, four.clone(), expected) + .await + .unwrap() + .into_inner(); + let ack = ManagedAppliedState::try_from(retry.applied.unwrap()).unwrap(); + assert_eq!(ack.policy, four); + assert_eq!(ack.policy.revision, 4); + let response = read_managed_policy(&state, &target) + .await + .unwrap() + .into_inner(); + assert_eq!( + ManagedDenyPolicy::try_from(response.retained_desired.unwrap().policy.unwrap()).unwrap(), + four + ); +} + +#[tokio::test] +async fn retained_desired_recovers_cancelled_apply_sim() { + let clock = ManualClock::new(); + let meta = SimMetadataStore::new(clock.clone(), Arc::new(SimEntropy::seeded(222))); + retained_recovery(meta, clock).await; +} +#[tokio::test] +async fn retained_desired_recovers_cancelled_apply_pg() { + let Some(db) = engram_testkit::pg::fresh_db().await else { + return; + }; + let clock = ManualClock::new(); + let meta = Arc::new( + db.store + .clone() + .with_clock(clock.clone()) + .with_entropy(Arc::new(SimEntropy::seeded(222))), + ); + retained_recovery(meta, clock).await; +} +fn host_record( + id: engram_core::HostId, + name: &str, + now: chrono::DateTime, +) -> HostRecord { + HostRecord { + id, + hostname: name.to_string(), + cloud_metadata: Default::default(), + capacity: HostCapacity { + total_gb: 100, + used_gb: 0, + total_mib: 32_768, + used_mib: 0, + running_sandboxes: 0, + }, + utilization: Default::default(), + status: HostStatus::Ready, + last_heartbeat_at: now, + host_addr: None, + ready_images: Vec::new(), + current_bundles: Vec::new(), + sandbox_bundles: Vec::new(), + cordoned: false, + cordon_owner: None, + cordon_reason: None, + retire_requested_at: None, + retired_at: None, + total_vcpus: 16, + wire_version: 1, + stages_images: false, + capabilities: Default::default(), + lease_expires_at: None, + lease_state: Default::default(), + lease_epoch: 0, + } +} diff --git a/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto b/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto index 64198593b..21e525196 100644 --- a/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto +++ b/crates/engram-protocol/proto/engram/mcp/v1/session_policy.proto @@ -113,6 +113,14 @@ message ReadManagedPolicyRequest { message ReadManagedPolicyResponse { uint32 protocol = 1; optional ManagedAppliedState applied = 2; + // Required presence: absence means an unsupported read contract, not no policy. + ManagedRetainedDesired retained_desired = 3; +} +// Coordinator persistence at the checked read target. This is never an applied ACK. +message ManagedRetainedDesired { + ManagedPolicyTarget target = 1; + // Absence inside a present wrapper means no retained policy at this read. + optional ManagedDenyPolicy policy = 2; } message ApplyManagedPolicyRequest { uint32 protocol = 1; diff --git a/crates/engram-protocol/src/managed_policy.rs b/crates/engram-protocol/src/managed_policy.rs index 280f0c76c..d1e7530d5 100644 --- a/crates/engram-protocol/src/managed_policy.rs +++ b/crates/engram-protocol/src/managed_policy.rs @@ -143,6 +143,32 @@ impl From for wire::ManagedNetworkProof { } } +impl wire::ReadManagedPolicyResponse { + /// Validate the complete trusted read, including explicit authoritative absence. + /// Retained desired state is a revision/union floor, never proof of application. + pub fn validate_for(&self, target: &core::ManagedPolicyTarget) -> Result<(), String> { + if self.protocol != wire::PROTOCOL { + return Err("unsupported managed read protocol".into()); + } + let retained = self.retained_desired.as_ref().ok_or_else(missing)?; + let actual: core::ManagedPolicyTarget = + retained.target.clone().ok_or_else(missing)?.try_into()?; + if actual != *target { + return Err("retained policy identity changed".into()); + } + if let Some(policy) = &retained.policy { + core::ManagedDenyPolicy::try_from(policy.clone())?; + } + if let Some(applied) = &self.applied { + let applied: core::ManagedAppliedState = applied.clone().try_into()?; + if applied.context.target != *target { + return Err("host policy identity changed".into()); + } + } + Ok(()) + } +} + /// The managed bincode surface is bounded and rejects unknown trailing fields. pub fn decode(bytes: &[u8]) -> Result { use bincode::Options; diff --git a/crates/engram-protocol/tests/mcp_policy_wire.rs b/crates/engram-protocol/tests/mcp_policy_wire.rs index 1f69cd6ce..4606ef902 100644 --- a/crates/engram-protocol/tests/mcp_policy_wire.rs +++ b/crates/engram-protocol/tests/mcp_policy_wire.rs @@ -20,6 +20,8 @@ proptest! { #[test] fn policy_decode_never_panics(bytes in proptest::collection::vec(any::(),0..=4096)) { let _=mcp::CreatePolicySessionRequest::decode(bytes.as_slice()); + let _=mcp::ReadManagedPolicyResponse::decode(bytes.as_slice()); + let _=mcp::ManagedRetainedDesired::decode(bytes.as_slice()); let _=mcp::ReadWritePolicyRequest::decode(bytes.as_slice()); let _=mcp::AcceptPlanExitRequest::decode(bytes.as_slice()); let _=mcp::ValidateDispatchFenceRequest::decode(bytes.as_slice()); @@ -31,3 +33,66 @@ proptest! { let _=mcp::RevokeResponse::decode(bytes.as_slice()); } } + +#[test] +fn managed_read_requires_explicit_canonical_retention_and_exact_target() { + use engram_core::{types::managed_deny::*, HostId, SandboxId, SessionId}; + let target = ManagedPolicyTarget { + session_id: SessionId::new(), + sandbox_id: SandboxId::new(), + host_id: HostId::new(), + binding_epoch: 1, + }; + let mut response = mcp::ReadManagedPolicyResponse { + protocol: mcp::PROTOCOL, + applied: None, + retained_desired: None, + }; + assert!( + response.validate_for(&target).is_err(), + "old server omission must fail closed" + ); + response.retained_desired = Some(mcp::ManagedRetainedDesired { + target: Some(target.clone().into()), + policy: None, + }); + response.validate_for(&target).unwrap(); // Explicit absence, with no fabricated ACK. + let policy = ManagedDenyPolicy::new(3, ["orphan.example".into()]).unwrap(); + response.retained_desired.as_mut().unwrap().policy = Some(policy.clone().into()); + let roundtrip = + mcp::ReadManagedPolicyResponse::decode(response.encode_to_vec().as_slice()).unwrap(); + roundtrip.validate_for(&target).unwrap(); + assert!(roundtrip.applied.is_none()); + let mut wrong = target.clone(); + wrong.binding_epoch += 1; + assert!(response.validate_for(&wrong).is_err()); + response + .retained_desired + .as_mut() + .unwrap() + .policy + .as_mut() + .unwrap() + .hosts + .push("UPPER.EXAMPLE".into()); + assert!( + response.validate_for(&target).is_err(), + "noncanonical retained bytes rejected" + ); + response.retained_desired.as_mut().unwrap().policy = Some(policy.clone().into()); + let context = ManagedPolicyContext { + target: wrong, + operator_generation: 1, + operator_identity: format!("sha256:{}", "a".repeat(64)), + binding_hash: None, + }; + response.applied = Some( + ManagedAppliedState::new(context, policy, "host".into(), "proxy".into(), None) + .unwrap() + .into(), + ); + assert!( + response.validate_for(&target).is_err(), + "applied target must agree independently" + ); +} diff --git a/docs/adr/0125-isolated-external-mcp.md b/docs/adr/0125-isolated-external-mcp.md index e671d8dfc..d26887b43 100644 --- a/docs/adr/0125-isolated-external-mcp.md +++ b/docs/adr/0125-isolated-external-mcp.md @@ -91,6 +91,15 @@ the desired state before proxy application, drains existing session connections, then saves an applied marker before ACK. Read and compare-and-apply bind the exact session, sandbox, physical epoch, host and proxy incarnations, binding hash, operator identity, revision and digest. A lost ACK cannot remove retained hosts. + +The trusted read returns retained coordinator state separately from the exact +host receipt. A required wrapper carries the checked physical read target and +an optional canonical retained policy; a missing wrapper is unsupported, not an +empty union. Consumers retain both policies and choose a revision above both. +Coordinator persistence is not proof of host application. Reads check placement +before host I/O and after reading the retained policy, without a transaction +across that I/O. + Managed host mode requires mTLS for the whole host mutation listener, exact coordinator certificate pins, and host-specific server identity verification. There is no simultaneous plaintext mutation listener or downgrade fallback. @@ -106,6 +115,12 @@ Managed control requests use separate local proxy streams; a session drain does not hold a global network lock. Directory ownership requires a persistent host volume and one managed host generation per directory in deployment. +Generic start checks all durable managed session associations for the actual +sandbox before any bind or policy write. A duplicate ordinary binding cannot +hide a managed record, including one retained after unbind. Supplied fence and +policy identities must agree with the requested sandbox; the explicit unfenced +sentinel remains available to ordinary legacy callers. + Until the strict restore producer exists, known managed sessions reject restore, base restore, resume, and agent start. The management marker is trusted host state, and a corrupt marker does not make the session ordinary. A fresh target diff --git a/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts b/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts index bc1be67ed..eb2980b02 100644 --- a/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts +++ b/orchestrator/src/gen/engram/mcp/v1/session_policy_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/session_policy.proto. */ export const file_engram_mcp_v1_session_policy: GenFile = /*@__PURE__*/ - fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCSJlChNNYW5hZ2VkUG9saWN5VGFyZ2V0EhIKCnNlc3Npb25faWQYASABKAkSEgoKc2FuZGJveF9pZBgCIAEoCRIPCgdob3N0X2lkGAMgASgJEhUKDWJpbmRpbmdfZXBvY2gYBCABKAQi4AEKFE1hbmFnZWRQb2xpY3lDb250ZXh0EjIKBnRhcmdldBgBIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeVRhcmdldBIbChNvcGVyYXRvcl9nZW5lcmF0aW9uGAIgASgEEhkKEW9wZXJhdG9yX2lkZW50aXR5GAMgASgJEjAKBXBoYXNlGAQgASgOMiEuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5UGhhc2USGQoMYmluZGluZ19oYXNoGAUgASgJSACIAQFCDwoNX2JpbmRpbmdfaGFzaCJVChFNYW5hZ2VkRGVueVBvbGljeRIPCgd2ZXJzaW9uGAEgASgNEhAKCHJldmlzaW9uGAIgASgEEg0KBWhvc3RzGAMgAygJEg4KBmRpZ2VzdBgEIAEoCSKWAgoTTWFuYWdlZEFwcGxpZWRTdGF0ZRI0Cgdjb250ZXh0GAEgASgLMiMuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5Q29udGV4dBIwCgZwb2xpY3kYAiABKAsyIC5lbmdyYW0ubWNwLnYxLk1hbmFnZWREZW55UG9saWN5EhgKEGhvc3RfaW5jYXJuYXRpb24YAyABKAkSGQoRcHJveHlfaW5jYXJuYXRpb24YBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSPgoNbmV0d29ya19wcm9vZhgGIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZE5ldHdvcmtQcm9vZkgAiAEBQhAKDl9uZXR3b3JrX3Byb29mImAKGFJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIyCgZ0YXJnZXQYAiABKAsyIi5lbmdyYW0ubWNwLnYxLk1hbmFnZWRQb2xpY3lUYXJnZXQicwoZUmVhZE1hbmFnZWRQb2xpY3lSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRI4CgdhcHBsaWVkGAIgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkQXBwbGllZFN0YXRlSACIAQFCCgoIX2FwcGxpZWQi3QEKGUFwcGx5TWFuYWdlZFBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SNAoHY29udGV4dBgCIAEoCzIjLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeUNvbnRleHQSMAoGcG9saWN5GAMgASgLMiAuZW5ncmFtLm1jcC52MS5NYW5hZ2VkRGVueVBvbGljeRI5CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZEFwcGxpZWRTdGF0ZUgAiAEBQgsKCV9leHBlY3RlZCJjChpBcHBseU1hbmFnZWRQb2xpY3lSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIzCgdhcHBsaWVkGAIgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkQXBwbGllZFN0YXRlInUKE01hbmFnZWROZXR3b3JrUHJvb2YSMgoGdGFyZ2V0GAEgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5VGFyZ2V0EhAKCGd1ZXN0X2lwGAIgASgJEhgKEG5ldHdvcmtfaWRlbnRpdHkYAyABKAkqfgoSTWFuYWdlZFBvbGljeVBoYXNlEiQKIE1BTkFHRURfUE9MSUNZX1BIQVNFX1VOU1BFQ0lGSUVEEAASIgoeTUFOQUdFRF9QT0xJQ1lfUEhBU0VfQk9PVFNUUkFQEAESHgoaTUFOQUdFRF9QT0xJQ1lfUEhBU0VfRklOQUwQAjKMBgoUU2Vzc2lvblBvbGljeVNlcnZpY2USZgoRUmVhZE1hbmFnZWRQb2xpY3kSJy5lbmdyYW0ubWNwLnYxLlJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBooLmVuZ3JhbS5tY3AudjEuUmVhZE1hbmFnZWRQb2xpY3lSZXNwb25zZRJpChJBcHBseU1hbmFnZWRQb2xpY3kSKC5lbmdyYW0ubWNwLnYxLkFwcGx5TWFuYWdlZFBvbGljeVJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkFwcGx5TWFuYWdlZFBvbGljeVJlc3BvbnNlEmwKE0NyZWF0ZVBvbGljeVNlc3Npb24SKS5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DcmVhdGVQb2xpY3lTZXNzaW9uUmVzcG9uc2USYAoPUmVhZFdyaXRlUG9saWN5EiUuZW5ncmFtLm1jcC52MS5SZWFkV3JpdGVQb2xpY3lSZXF1ZXN0GiYuZW5ncmFtLm1jcC52MS5SZWFkV3JpdGVQb2xpY3lSZXNwb25zZRJdCg5BY2NlcHRQbGFuRXhpdBIkLmVuZ3JhbS5tY3AudjEuQWNjZXB0UGxhbkV4aXRSZXF1ZXN0GiUuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlc3BvbnNlEn4KGVZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHkSLy5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHlSZXF1ZXN0GjAuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVzcG9uc2UScgoVVmFsaWRhdGVEaXNwYXRjaEZlbmNlEisuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZWIGcHJvdG8z", [file_engram_app_v1_session]); + fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCSJlChNNYW5hZ2VkUG9saWN5VGFyZ2V0EhIKCnNlc3Npb25faWQYASABKAkSEgoKc2FuZGJveF9pZBgCIAEoCRIPCgdob3N0X2lkGAMgASgJEhUKDWJpbmRpbmdfZXBvY2gYBCABKAQi4AEKFE1hbmFnZWRQb2xpY3lDb250ZXh0EjIKBnRhcmdldBgBIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeVRhcmdldBIbChNvcGVyYXRvcl9nZW5lcmF0aW9uGAIgASgEEhkKEW9wZXJhdG9yX2lkZW50aXR5GAMgASgJEjAKBXBoYXNlGAQgASgOMiEuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5UGhhc2USGQoMYmluZGluZ19oYXNoGAUgASgJSACIAQFCDwoNX2JpbmRpbmdfaGFzaCJVChFNYW5hZ2VkRGVueVBvbGljeRIPCgd2ZXJzaW9uGAEgASgNEhAKCHJldmlzaW9uGAIgASgEEg0KBWhvc3RzGAMgAygJEg4KBmRpZ2VzdBgEIAEoCSKWAgoTTWFuYWdlZEFwcGxpZWRTdGF0ZRI0Cgdjb250ZXh0GAEgASgLMiMuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5Q29udGV4dBIwCgZwb2xpY3kYAiABKAsyIC5lbmdyYW0ubWNwLnYxLk1hbmFnZWREZW55UG9saWN5EhgKEGhvc3RfaW5jYXJuYXRpb24YAyABKAkSGQoRcHJveHlfaW5jYXJuYXRpb24YBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSPgoNbmV0d29ya19wcm9vZhgGIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZE5ldHdvcmtQcm9vZkgAiAEBQhAKDl9uZXR3b3JrX3Byb29mImAKGFJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIyCgZ0YXJnZXQYAiABKAsyIi5lbmdyYW0ubWNwLnYxLk1hbmFnZWRQb2xpY3lUYXJnZXQitAEKGVJlYWRNYW5hZ2VkUG9saWN5UmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SOAoHYXBwbGllZBgCIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZEFwcGxpZWRTdGF0ZUgAiAEBEj8KEHJldGFpbmVkX2Rlc2lyZWQYAyABKAsyJS5lbmdyYW0ubWNwLnYxLk1hbmFnZWRSZXRhaW5lZERlc2lyZWRCCgoIX2FwcGxpZWQijgEKFk1hbmFnZWRSZXRhaW5lZERlc2lyZWQSMgoGdGFyZ2V0GAEgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5VGFyZ2V0EjUKBnBvbGljeRgCIAEoCzIgLmVuZ3JhbS5tY3AudjEuTWFuYWdlZERlbnlQb2xpY3lIAIgBAUIJCgdfcG9saWN5It0BChlBcHBseU1hbmFnZWRQb2xpY3lSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjQKB2NvbnRleHQYAiABKAsyIy5lbmdyYW0ubWNwLnYxLk1hbmFnZWRQb2xpY3lDb250ZXh0EjAKBnBvbGljeRgDIAEoCzIgLmVuZ3JhbS5tY3AudjEuTWFuYWdlZERlbnlQb2xpY3kSOQoIZXhwZWN0ZWQYBCABKAsyIi5lbmdyYW0ubWNwLnYxLk1hbmFnZWRBcHBsaWVkU3RhdGVIAIgBAUILCglfZXhwZWN0ZWQiYwoaQXBwbHlNYW5hZ2VkUG9saWN5UmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SMwoHYXBwbGllZBgCIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZEFwcGxpZWRTdGF0ZSJ1ChNNYW5hZ2VkTmV0d29ya1Byb29mEjIKBnRhcmdldBgBIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeVRhcmdldBIQCghndWVzdF9pcBgCIAEoCRIYChBuZXR3b3JrX2lkZW50aXR5GAMgASgJKn4KEk1hbmFnZWRQb2xpY3lQaGFzZRIkCiBNQU5BR0VEX1BPTElDWV9QSEFTRV9VTlNQRUNJRklFRBAAEiIKHk1BTkFHRURfUE9MSUNZX1BIQVNFX0JPT1RTVFJBUBABEh4KGk1BTkFHRURfUE9MSUNZX1BIQVNFX0ZJTkFMEAIyjAYKFFNlc3Npb25Qb2xpY3lTZXJ2aWNlEmYKEVJlYWRNYW5hZ2VkUG9saWN5EicuZW5ncmFtLm1jcC52MS5SZWFkTWFuYWdlZFBvbGljeVJlcXVlc3QaKC5lbmdyYW0ubWNwLnYxLlJlYWRNYW5hZ2VkUG9saWN5UmVzcG9uc2USaQoSQXBwbHlNYW5hZ2VkUG9saWN5EiguZW5ncmFtLm1jcC52MS5BcHBseU1hbmFnZWRQb2xpY3lSZXF1ZXN0GikuZW5ncmFtLm1jcC52MS5BcHBseU1hbmFnZWRQb2xpY3lSZXNwb25zZRJsChNDcmVhdGVQb2xpY3lTZXNzaW9uEikuZW5ncmFtLm1jcC52MS5DcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBoqLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEmAKD1JlYWRXcml0ZVBvbGljeRIlLmVuZ3JhbS5tY3AudjEuUmVhZFdyaXRlUG9saWN5UmVxdWVzdBomLmVuZ3JhbS5tY3AudjEuUmVhZFdyaXRlUG9saWN5UmVzcG9uc2USXQoOQWNjZXB0UGxhbkV4aXQSJC5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVxdWVzdBolLmVuZ3JhbS5tY3AudjEuQWNjZXB0UGxhbkV4aXRSZXNwb25zZRJ+ChlWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5Ei8uZW5ncmFtLm1jcC52MS5WYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBowLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEnIKFVZhbGlkYXRlRGlzcGF0Y2hGZW5jZRIrLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVEaXNwYXRjaEZlbmNlUmVxdWVzdBosLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVEaXNwYXRjaEZlbmNlUmVzcG9uc2ViBnByb3RvMw", [file_engram_app_v1_session]); /** * @generated from message engram.mcp.v1.CreatePolicySessionRequest @@ -497,6 +497,13 @@ export type ReadManagedPolicyResponse = Message<"engram.mcp.v1.ReadManagedPolicy * @generated from field: optional engram.mcp.v1.ManagedAppliedState applied = 2; */ applied?: ManagedAppliedState; + + /** + * Required presence: absence means an unsupported read contract, not no policy. + * + * @generated from field: engram.mcp.v1.ManagedRetainedDesired retained_desired = 3; + */ + retainedDesired?: ManagedRetainedDesired; }; /** @@ -506,6 +513,32 @@ export type ReadManagedPolicyResponse = Message<"engram.mcp.v1.ReadManagedPolicy export const ReadManagedPolicyResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_session_policy, 15); +/** + * Coordinator persistence at the checked read target. This is never an applied ACK. + * + * @generated from message engram.mcp.v1.ManagedRetainedDesired + */ +export type ManagedRetainedDesired = Message<"engram.mcp.v1.ManagedRetainedDesired"> & { + /** + * @generated from field: engram.mcp.v1.ManagedPolicyTarget target = 1; + */ + target?: ManagedPolicyTarget; + + /** + * Absence inside a present wrapper means no retained policy at this read. + * + * @generated from field: optional engram.mcp.v1.ManagedDenyPolicy policy = 2; + */ + policy?: ManagedDenyPolicy; +}; + +/** + * Describes the message engram.mcp.v1.ManagedRetainedDesired. + * Use `create(ManagedRetainedDesiredSchema)` to create a new message. + */ +export const ManagedRetainedDesiredSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 16); + /** * @generated from message engram.mcp.v1.ApplyManagedPolicyRequest */ @@ -536,7 +569,7 @@ export type ApplyManagedPolicyRequest = Message<"engram.mcp.v1.ApplyManagedPolic * Use `create(ApplyManagedPolicyRequestSchema)` to create a new message. */ export const ApplyManagedPolicyRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_mcp_v1_session_policy, 16); + messageDesc(file_engram_mcp_v1_session_policy, 17); /** * @generated from message engram.mcp.v1.ApplyManagedPolicyResponse @@ -558,7 +591,7 @@ export type ApplyManagedPolicyResponse = Message<"engram.mcp.v1.ApplyManagedPoli * Use `create(ApplyManagedPolicyResponseSchema)` to create a new message. */ export const ApplyManagedPolicyResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_mcp_v1_session_policy, 17); + messageDesc(file_engram_mcp_v1_session_policy, 18); /** * @generated from message engram.mcp.v1.ManagedNetworkProof @@ -585,7 +618,7 @@ export type ManagedNetworkProof = Message<"engram.mcp.v1.ManagedNetworkProof"> & * Use `create(ManagedNetworkProofSchema)` to create a new message. */ export const ManagedNetworkProofSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_mcp_v1_session_policy, 18); + messageDesc(file_engram_mcp_v1_session_policy, 19); /** * These messages describe a host receipt, not permission to expose guest tools. diff --git a/web/src/gen/engram/mcp/v1/session_policy_pb.ts b/web/src/gen/engram/mcp/v1/session_policy_pb.ts index bc1be67ed..eb2980b02 100644 --- a/web/src/gen/engram/mcp/v1/session_policy_pb.ts +++ b/web/src/gen/engram/mcp/v1/session_policy_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/mcp/v1/session_policy.proto. */ export const file_engram_mcp_v1_session_policy: GenFile = /*@__PURE__*/ - fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCSJlChNNYW5hZ2VkUG9saWN5VGFyZ2V0EhIKCnNlc3Npb25faWQYASABKAkSEgoKc2FuZGJveF9pZBgCIAEoCRIPCgdob3N0X2lkGAMgASgJEhUKDWJpbmRpbmdfZXBvY2gYBCABKAQi4AEKFE1hbmFnZWRQb2xpY3lDb250ZXh0EjIKBnRhcmdldBgBIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeVRhcmdldBIbChNvcGVyYXRvcl9nZW5lcmF0aW9uGAIgASgEEhkKEW9wZXJhdG9yX2lkZW50aXR5GAMgASgJEjAKBXBoYXNlGAQgASgOMiEuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5UGhhc2USGQoMYmluZGluZ19oYXNoGAUgASgJSACIAQFCDwoNX2JpbmRpbmdfaGFzaCJVChFNYW5hZ2VkRGVueVBvbGljeRIPCgd2ZXJzaW9uGAEgASgNEhAKCHJldmlzaW9uGAIgASgEEg0KBWhvc3RzGAMgAygJEg4KBmRpZ2VzdBgEIAEoCSKWAgoTTWFuYWdlZEFwcGxpZWRTdGF0ZRI0Cgdjb250ZXh0GAEgASgLMiMuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5Q29udGV4dBIwCgZwb2xpY3kYAiABKAsyIC5lbmdyYW0ubWNwLnYxLk1hbmFnZWREZW55UG9saWN5EhgKEGhvc3RfaW5jYXJuYXRpb24YAyABKAkSGQoRcHJveHlfaW5jYXJuYXRpb24YBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSPgoNbmV0d29ya19wcm9vZhgGIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZE5ldHdvcmtQcm9vZkgAiAEBQhAKDl9uZXR3b3JrX3Byb29mImAKGFJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIyCgZ0YXJnZXQYAiABKAsyIi5lbmdyYW0ubWNwLnYxLk1hbmFnZWRQb2xpY3lUYXJnZXQicwoZUmVhZE1hbmFnZWRQb2xpY3lSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRI4CgdhcHBsaWVkGAIgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkQXBwbGllZFN0YXRlSACIAQFCCgoIX2FwcGxpZWQi3QEKGUFwcGx5TWFuYWdlZFBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SNAoHY29udGV4dBgCIAEoCzIjLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeUNvbnRleHQSMAoGcG9saWN5GAMgASgLMiAuZW5ncmFtLm1jcC52MS5NYW5hZ2VkRGVueVBvbGljeRI5CghleHBlY3RlZBgEIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZEFwcGxpZWRTdGF0ZUgAiAEBQgsKCV9leHBlY3RlZCJjChpBcHBseU1hbmFnZWRQb2xpY3lSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIzCgdhcHBsaWVkGAIgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkQXBwbGllZFN0YXRlInUKE01hbmFnZWROZXR3b3JrUHJvb2YSMgoGdGFyZ2V0GAEgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5VGFyZ2V0EhAKCGd1ZXN0X2lwGAIgASgJEhgKEG5ldHdvcmtfaWRlbnRpdHkYAyABKAkqfgoSTWFuYWdlZFBvbGljeVBoYXNlEiQKIE1BTkFHRURfUE9MSUNZX1BIQVNFX1VOU1BFQ0lGSUVEEAASIgoeTUFOQUdFRF9QT0xJQ1lfUEhBU0VfQk9PVFNUUkFQEAESHgoaTUFOQUdFRF9QT0xJQ1lfUEhBU0VfRklOQUwQAjKMBgoUU2Vzc2lvblBvbGljeVNlcnZpY2USZgoRUmVhZE1hbmFnZWRQb2xpY3kSJy5lbmdyYW0ubWNwLnYxLlJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBooLmVuZ3JhbS5tY3AudjEuUmVhZE1hbmFnZWRQb2xpY3lSZXNwb25zZRJpChJBcHBseU1hbmFnZWRQb2xpY3kSKC5lbmdyYW0ubWNwLnYxLkFwcGx5TWFuYWdlZFBvbGljeVJlcXVlc3QaKS5lbmdyYW0ubWNwLnYxLkFwcGx5TWFuYWdlZFBvbGljeVJlc3BvbnNlEmwKE0NyZWF0ZVBvbGljeVNlc3Npb24SKS5lbmdyYW0ubWNwLnYxLkNyZWF0ZVBvbGljeVNlc3Npb25SZXF1ZXN0GiouZW5ncmFtLm1jcC52MS5DcmVhdGVQb2xpY3lTZXNzaW9uUmVzcG9uc2USYAoPUmVhZFdyaXRlUG9saWN5EiUuZW5ncmFtLm1jcC52MS5SZWFkV3JpdGVQb2xpY3lSZXF1ZXN0GiYuZW5ncmFtLm1jcC52MS5SZWFkV3JpdGVQb2xpY3lSZXNwb25zZRJdCg5BY2NlcHRQbGFuRXhpdBIkLmVuZ3JhbS5tY3AudjEuQWNjZXB0UGxhbkV4aXRSZXF1ZXN0GiUuZW5ncmFtLm1jcC52MS5BY2NlcHRQbGFuRXhpdFJlc3BvbnNlEn4KGVZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHkSLy5lbmdyYW0ubWNwLnYxLlZhbGlkYXRlRXhlY3V0aW9uSWRlbnRpdHlSZXF1ZXN0GjAuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVzcG9uc2UScgoVVmFsaWRhdGVEaXNwYXRjaEZlbmNlEisuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0GiwuZW5ncmFtLm1jcC52MS5WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZWIGcHJvdG8z", [file_engram_app_v1_session]); + fileDesc("CiJlbmdyYW0vbWNwL3YxL3Nlc3Npb25fcG9saWN5LnByb3RvEg1lbmdyYW0ubWNwLnYxIr0BChpDcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIzCgZjcmVhdGUYAiABKAsyIy5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXF1ZXN0EhAKCGFjdG9yX2lkGAMgASgJEhAKCGlucHV0X2lkGAQgASgJEh4KEXBhcmVudF9zZXNzaW9uX2lkGAUgASgJSACIAQFCFAoSX3BhcmVudF9zZXNzaW9uX2lkInQKFlJlYWRXcml0ZVBvbGljeVJlcXVlc3QSEAoIcHJvdG9jb2wYASABKA0SEgoKc2Vzc2lvbl9pZBgCIAEoCRIeChFwbGFuX3Rvb2xfY2FsbF9pZBgDIAEoCUgAiAEBQhQKEl9wbGFuX3Rvb2xfY2FsbF9pZCKAAQoVQWNjZXB0UGxhbkV4aXRSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEi4KCGV4cGVjdGVkGAIgASgLMhwuZW5ncmFtLmFwcC52MS5NY3BQbGFuUmV2aWV3EhAKCGFjdG9yX2lkGAMgASgJEhMKC3Jlc3VsdF9qc29uGAQgASgJIsICChxWYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRI9ChNhY2NlcHRlZF9wb2xpY3lfcmVmGAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BXcml0ZVBvbGljeVJlZhIiChphY2NlcHRlZF93cml0ZV9lbGlnaWJpbGl0eRgGIAEoCRI+ChRhbmNlc3Rvcl9wb2xpY3lfcmVmcxgHIAMoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSFwoPbW9kZWxfdG9vbF9uYW1lGAggASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgJIAEoCSJBCh1WYWxpZGF0ZURpc3BhdGNoRmVuY2VSZXNwb25zZRIQCghwcm90b2NvbBgBIAEoDRIOCgZzdGF0dXMYAiABKAkiVAobQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEjUKB3Nlc3Npb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkNyZWF0ZVNlc3Npb25SZXNwb25zZSJSChdSZWFkV3JpdGVQb2xpY3lSZXNwb25zZRI3CgVzdGF0ZRgBIAEoCzIoLmVuZ3JhbS5hcHAudjEuR2V0TWNwV3JpdGVQb2xpY3lSZXNwb25zZSJOChZBY2NlcHRQbGFuRXhpdFJlc3BvbnNlEjQKBnBvbGljeRgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuTWNwVHJ1c3RlZFdyaXRlUG9saWN5IsYCCiBWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRISCgpzZXNzaW9uX2lkGAIgASgJEg4KBnJ1bl9pZBgDIAEoCRIUCgx0b29sX2NhbGxfaWQYBCABKAkSPQoTYWNjZXB0ZWRfcG9saWN5X3JlZhgFIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwV3JpdGVQb2xpY3lSZWYSIgoaYWNjZXB0ZWRfd3JpdGVfZWxpZ2liaWxpdHkYBiABKAkSPgoUYW5jZXN0b3JfcG9saWN5X3JlZnMYByADKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFdyaXRlUG9saWN5UmVmEhcKD21vZGVsX3Rvb2xfbmFtZRgIIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYCSABKAkiRQohVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEhAKCHByb3RvY29sGAEgASgNEg4KBnN0YXR1cxgCIAEoCSJlChNNYW5hZ2VkUG9saWN5VGFyZ2V0EhIKCnNlc3Npb25faWQYASABKAkSEgoKc2FuZGJveF9pZBgCIAEoCRIPCgdob3N0X2lkGAMgASgJEhUKDWJpbmRpbmdfZXBvY2gYBCABKAQi4AEKFE1hbmFnZWRQb2xpY3lDb250ZXh0EjIKBnRhcmdldBgBIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeVRhcmdldBIbChNvcGVyYXRvcl9nZW5lcmF0aW9uGAIgASgEEhkKEW9wZXJhdG9yX2lkZW50aXR5GAMgASgJEjAKBXBoYXNlGAQgASgOMiEuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5UGhhc2USGQoMYmluZGluZ19oYXNoGAUgASgJSACIAQFCDwoNX2JpbmRpbmdfaGFzaCJVChFNYW5hZ2VkRGVueVBvbGljeRIPCgd2ZXJzaW9uGAEgASgNEhAKCHJldmlzaW9uGAIgASgEEg0KBWhvc3RzGAMgAygJEg4KBmRpZ2VzdBgEIAEoCSKWAgoTTWFuYWdlZEFwcGxpZWRTdGF0ZRI0Cgdjb250ZXh0GAEgASgLMiMuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5Q29udGV4dBIwCgZwb2xpY3kYAiABKAsyIC5lbmdyYW0ubWNwLnYxLk1hbmFnZWREZW55UG9saWN5EhgKEGhvc3RfaW5jYXJuYXRpb24YAyABKAkSGQoRcHJveHlfaW5jYXJuYXRpb24YBCABKAkSEAoIc3RhdGVfaWQYBSABKAkSPgoNbmV0d29ya19wcm9vZhgGIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZE5ldHdvcmtQcm9vZkgAiAEBQhAKDl9uZXR3b3JrX3Byb29mImAKGFJlYWRNYW5hZ2VkUG9saWN5UmVxdWVzdBIQCghwcm90b2NvbBgBIAEoDRIyCgZ0YXJnZXQYAiABKAsyIi5lbmdyYW0ubWNwLnYxLk1hbmFnZWRQb2xpY3lUYXJnZXQitAEKGVJlYWRNYW5hZ2VkUG9saWN5UmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SOAoHYXBwbGllZBgCIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZEFwcGxpZWRTdGF0ZUgAiAEBEj8KEHJldGFpbmVkX2Rlc2lyZWQYAyABKAsyJS5lbmdyYW0ubWNwLnYxLk1hbmFnZWRSZXRhaW5lZERlc2lyZWRCCgoIX2FwcGxpZWQijgEKFk1hbmFnZWRSZXRhaW5lZERlc2lyZWQSMgoGdGFyZ2V0GAEgASgLMiIuZW5ncmFtLm1jcC52MS5NYW5hZ2VkUG9saWN5VGFyZ2V0EjUKBnBvbGljeRgCIAEoCzIgLmVuZ3JhbS5tY3AudjEuTWFuYWdlZERlbnlQb2xpY3lIAIgBAUIJCgdfcG9saWN5It0BChlBcHBseU1hbmFnZWRQb2xpY3lSZXF1ZXN0EhAKCHByb3RvY29sGAEgASgNEjQKB2NvbnRleHQYAiABKAsyIy5lbmdyYW0ubWNwLnYxLk1hbmFnZWRQb2xpY3lDb250ZXh0EjAKBnBvbGljeRgDIAEoCzIgLmVuZ3JhbS5tY3AudjEuTWFuYWdlZERlbnlQb2xpY3kSOQoIZXhwZWN0ZWQYBCABKAsyIi5lbmdyYW0ubWNwLnYxLk1hbmFnZWRBcHBsaWVkU3RhdGVIAIgBAUILCglfZXhwZWN0ZWQiYwoaQXBwbHlNYW5hZ2VkUG9saWN5UmVzcG9uc2USEAoIcHJvdG9jb2wYASABKA0SMwoHYXBwbGllZBgCIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZEFwcGxpZWRTdGF0ZSJ1ChNNYW5hZ2VkTmV0d29ya1Byb29mEjIKBnRhcmdldBgBIAEoCzIiLmVuZ3JhbS5tY3AudjEuTWFuYWdlZFBvbGljeVRhcmdldBIQCghndWVzdF9pcBgCIAEoCRIYChBuZXR3b3JrX2lkZW50aXR5GAMgASgJKn4KEk1hbmFnZWRQb2xpY3lQaGFzZRIkCiBNQU5BR0VEX1BPTElDWV9QSEFTRV9VTlNQRUNJRklFRBAAEiIKHk1BTkFHRURfUE9MSUNZX1BIQVNFX0JPT1RTVFJBUBABEh4KGk1BTkFHRURfUE9MSUNZX1BIQVNFX0ZJTkFMEAIyjAYKFFNlc3Npb25Qb2xpY3lTZXJ2aWNlEmYKEVJlYWRNYW5hZ2VkUG9saWN5EicuZW5ncmFtLm1jcC52MS5SZWFkTWFuYWdlZFBvbGljeVJlcXVlc3QaKC5lbmdyYW0ubWNwLnYxLlJlYWRNYW5hZ2VkUG9saWN5UmVzcG9uc2USaQoSQXBwbHlNYW5hZ2VkUG9saWN5EiguZW5ncmFtLm1jcC52MS5BcHBseU1hbmFnZWRQb2xpY3lSZXF1ZXN0GikuZW5ncmFtLm1jcC52MS5BcHBseU1hbmFnZWRQb2xpY3lSZXNwb25zZRJsChNDcmVhdGVQb2xpY3lTZXNzaW9uEikuZW5ncmFtLm1jcC52MS5DcmVhdGVQb2xpY3lTZXNzaW9uUmVxdWVzdBoqLmVuZ3JhbS5tY3AudjEuQ3JlYXRlUG9saWN5U2Vzc2lvblJlc3BvbnNlEmAKD1JlYWRXcml0ZVBvbGljeRIlLmVuZ3JhbS5tY3AudjEuUmVhZFdyaXRlUG9saWN5UmVxdWVzdBomLmVuZ3JhbS5tY3AudjEuUmVhZFdyaXRlUG9saWN5UmVzcG9uc2USXQoOQWNjZXB0UGxhbkV4aXQSJC5lbmdyYW0ubWNwLnYxLkFjY2VwdFBsYW5FeGl0UmVxdWVzdBolLmVuZ3JhbS5tY3AudjEuQWNjZXB0UGxhbkV4aXRSZXNwb25zZRJ+ChlWYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5Ei8uZW5ncmFtLm1jcC52MS5WYWxpZGF0ZUV4ZWN1dGlvbklkZW50aXR5UmVxdWVzdBowLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVFeGVjdXRpb25JZGVudGl0eVJlc3BvbnNlEnIKFVZhbGlkYXRlRGlzcGF0Y2hGZW5jZRIrLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVEaXNwYXRjaEZlbmNlUmVxdWVzdBosLmVuZ3JhbS5tY3AudjEuVmFsaWRhdGVEaXNwYXRjaEZlbmNlUmVzcG9uc2ViBnByb3RvMw", [file_engram_app_v1_session]); /** * @generated from message engram.mcp.v1.CreatePolicySessionRequest @@ -497,6 +497,13 @@ export type ReadManagedPolicyResponse = Message<"engram.mcp.v1.ReadManagedPolicy * @generated from field: optional engram.mcp.v1.ManagedAppliedState applied = 2; */ applied?: ManagedAppliedState; + + /** + * Required presence: absence means an unsupported read contract, not no policy. + * + * @generated from field: engram.mcp.v1.ManagedRetainedDesired retained_desired = 3; + */ + retainedDesired?: ManagedRetainedDesired; }; /** @@ -506,6 +513,32 @@ export type ReadManagedPolicyResponse = Message<"engram.mcp.v1.ReadManagedPolicy export const ReadManagedPolicyResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_mcp_v1_session_policy, 15); +/** + * Coordinator persistence at the checked read target. This is never an applied ACK. + * + * @generated from message engram.mcp.v1.ManagedRetainedDesired + */ +export type ManagedRetainedDesired = Message<"engram.mcp.v1.ManagedRetainedDesired"> & { + /** + * @generated from field: engram.mcp.v1.ManagedPolicyTarget target = 1; + */ + target?: ManagedPolicyTarget; + + /** + * Absence inside a present wrapper means no retained policy at this read. + * + * @generated from field: optional engram.mcp.v1.ManagedDenyPolicy policy = 2; + */ + policy?: ManagedDenyPolicy; +}; + +/** + * Describes the message engram.mcp.v1.ManagedRetainedDesired. + * Use `create(ManagedRetainedDesiredSchema)` to create a new message. + */ +export const ManagedRetainedDesiredSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_mcp_v1_session_policy, 16); + /** * @generated from message engram.mcp.v1.ApplyManagedPolicyRequest */ @@ -536,7 +569,7 @@ export type ApplyManagedPolicyRequest = Message<"engram.mcp.v1.ApplyManagedPolic * Use `create(ApplyManagedPolicyRequestSchema)` to create a new message. */ export const ApplyManagedPolicyRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_mcp_v1_session_policy, 16); + messageDesc(file_engram_mcp_v1_session_policy, 17); /** * @generated from message engram.mcp.v1.ApplyManagedPolicyResponse @@ -558,7 +591,7 @@ export type ApplyManagedPolicyResponse = Message<"engram.mcp.v1.ApplyManagedPoli * Use `create(ApplyManagedPolicyResponseSchema)` to create a new message. */ export const ApplyManagedPolicyResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_mcp_v1_session_policy, 17); + messageDesc(file_engram_mcp_v1_session_policy, 18); /** * @generated from message engram.mcp.v1.ManagedNetworkProof @@ -585,7 +618,7 @@ export type ManagedNetworkProof = Message<"engram.mcp.v1.ManagedNetworkProof"> & * Use `create(ManagedNetworkProofSchema)` to create a new message. */ export const ManagedNetworkProofSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_mcp_v1_session_policy, 18); + messageDesc(file_engram_mcp_v1_session_policy, 19); /** * These messages describe a host receipt, not permission to expose guest tools. From 06d4dcf53584a1b39c6ac9820affe2bb7b083e42 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 11:24:32 -0500 Subject: [PATCH 032/279] Add allocation-bound private sandbox networking Route a bounded per-VM gVisor helper through the existing TLS and DNS policy paths using inherited Unix descriptors. Keep policy drains pending until helper cleanup EOF, and retain a closed session gate if that barrier fails. --- crates/engram-egress-proto/src/lib.rs | 5 +- crates/engram-egress-proto/src/private.rs | 383 ++++++++ crates/engram-egress-proxy/src/connections.rs | 25 +- crates/engram-egress-proxy/src/dns.rs | 37 + crates/engram-egress-proxy/src/proxy.rs | 47 +- crates/engram-egress-proxy/src/registry.rs | 26 + crates/engram-egress-proxyd/src/control.rs | 160 +++- crates/engram-egress-proxyd/src/lib.rs | 22 +- crates/engram-egress-proxyd/src/private.rs | 893 ++++++++++++++++++ crates/engram-host-agent/src/proxyd_client.rs | 14 + docs/adr/0125-isolated-external-mcp.md | 35 + sandbox-netstack/README.md | 154 +++ sandbox-netstack/build.sh | 27 + sandbox-netstack/frame.go | 92 ++ sandbox-netstack/frame_test.go | 78 ++ sandbox-netstack/go.mod | 15 + sandbox-netstack/go.sum | 12 + sandbox-netstack/helper_test.go | 540 +++++++++++ sandbox-netstack/main.go | 501 ++++++++++ sandbox-netstack/protocol.go | 225 +++++ 20 files changed, 3274 insertions(+), 17 deletions(-) create mode 100644 crates/engram-egress-proto/src/private.rs create mode 100644 crates/engram-egress-proxyd/src/private.rs create mode 100644 sandbox-netstack/README.md create mode 100755 sandbox-netstack/build.sh create mode 100644 sandbox-netstack/frame.go create mode 100644 sandbox-netstack/frame_test.go create mode 100644 sandbox-netstack/go.mod create mode 100644 sandbox-netstack/go.sum create mode 100644 sandbox-netstack/helper_test.go create mode 100644 sandbox-netstack/main.go create mode 100644 sandbox-netstack/protocol.go diff --git a/crates/engram-egress-proto/src/lib.rs b/crates/engram-egress-proto/src/lib.rs index 8e3d89abd..efe7c6587 100644 --- a/crates/engram-egress-proto/src/lib.rs +++ b/crates/engram-egress-proto/src/lib.rs @@ -36,6 +36,7 @@ pub mod adopt; pub mod manifest; +pub mod private; use engram_core::types::egress::SessionEgressPolicy; use engram_core::{SandboxId, SessionId}; @@ -44,7 +45,7 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; /// Bump on any wire-incompatible change. A mismatch in `Hello` /// position is never adopted — the caller kills and respawns. -pub const PROTO_VERSION: u32 = 2; +pub const PROTO_VERSION: u32 = 3; /// Frame size cap. `SyncPolicies` carries every live session's policy /// (resolved secrets included), so the cap is generous; both ends are @@ -85,6 +86,8 @@ pub const BUILD_ENV_SOURCE_FINGERPRINT: &str = "ENGRAM_EGRESS_PROXYD_FINGERPRINT /// Host-agent → proxyd. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum ToProxyd { + /// Upgrade this host-only connection. A bounded private Allocation follows. + PrivateNetwork, ListManaged, ReadManaged(engram_core::SessionId), QuarantineManaged(engram_core::SessionId), diff --git a/crates/engram-egress-proto/src/private.rs b/crates/engram-egress-proto/src/private.rs new file mode 100644 index 000000000..b6c510dd8 --- /dev/null +++ b/crates/engram-egress-proto/src/private.rs @@ -0,0 +1,383 @@ +//! Host-private network transport. The connected stream is the capability. +//! No value in a guest packet selects an allocation or session. +use engram_core::{HostId, SandboxId, SessionId}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{io, net::Ipv4Addr}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +pub const VERSION: u32 = 1; +pub const MAX_HEADER: usize = 2048; +pub const MAX_DNS: usize = 4096; +pub const MAX_FLOWS: usize = 64; +pub const MAX_DNS_FLOWS: usize = 8; +pub const HANDOFF_SECONDS: u64 = 5; + +/// Host-only registration, checked against the live proxy and policy. The +/// placement epoch is deliberately absent: registration is always CLOSED. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Allocation { + pub version: u32, + pub host_id: HostId, + pub host_incarnation: String, + pub proxy_incarnation: String, + pub session_id: SessionId, + pub sandbox_id: SandboxId, + pub boot_attempt: String, + pub attachment_generation: u64, + pub guest_ip: Ipv4Addr, + pub gateway_ip: Ipv4Addr, + pub policy_revision: u64, + pub policy_digest: String, +} +impl Allocation { + pub fn validate(&self) -> io::Result<()> { + let bounded = |s: &str| { + !s.is_empty() + && s.len() <= 128 + && s.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-') + }; + if self.version != VERSION + || self.host_id.as_uuid().is_nil() + || self.session_id.as_uuid().is_nil() + || self.sandbox_id.as_uuid().is_nil() + || !bounded(&self.host_incarnation) + || !bounded(&self.proxy_incarnation) + || !bounded(&self.boot_attempt) + || self.attachment_generation == 0 + || self.guest_ip == self.gateway_ip + || self.guest_ip.is_unspecified() + || self.guest_ip.is_multicast() + || self.gateway_ip.is_unspecified() + || self.gateway_ip.is_multicast() + || !engram_core::types::managed_deny::is_digest(&self.policy_digest) + { + return Err(invalid()); + } + Ok(()) + } +} + +/// Strict monotonic sequence on one allocation stream. Bind can occur once. +/// A TCP or DNS header is immediately followed by one SCM_RIGHTS carrier. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Request { + pub version: u32, + pub sequence: u64, + pub epoch: u64, + pub operation: Operation, +} +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum Operation { + Bind, + Tcp { destination: Ipv4Addr, port: u16 }, + Dns { destination: Ipv4Addr, port: u16 }, + Close, +} +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Reply { + pub version: u32, + pub sequence: u64, + pub epoch: u64, + pub state: String, +} + +pub fn invalid() -> io::Error { + io::Error::new( + io::ErrorKind::InvalidData, + "invalid private network transport", + ) +} +pub async fn read(r: &mut R) -> io::Result { + let mut prefix = [0; 4]; + r.read_exact(&mut prefix[..1]).await?; + tokio::time::timeout(std::time::Duration::from_secs(HANDOFF_SECONDS), async { + r.read_exact(&mut prefix[1..]).await?; + let len = u32::from_be_bytes(prefix) as usize; + if len == 0 || len > MAX_HEADER { + return Err(invalid()); + } + let mut body = vec![0; len]; + r.read_exact(&mut body).await?; + serde_json::from_slice(&body).map_err(|_| invalid()) + }) + .await + .map_err(|_| invalid())? +} +pub async fn write(w: &mut W, v: &T) -> io::Result<()> { + let body = serde_json::to_vec(v).map_err(|_| invalid())?; + if body.is_empty() || body.len() > MAX_HEADER { + return Err(invalid()); + } + tokio::time::timeout(std::time::Duration::from_secs(HANDOFF_SECONDS), async { + w.write_all(&(body.len() as u32).to_be_bytes()).await?; + w.write_all(&body).await?; + w.flush().await + }) + .await + .map_err(|_| invalid())? +} + +/// Register a fresh capability on a dedicated host control connection. Never +/// retry: loss of the reply makes this attempt unusable. The returned socket is +/// for one helper only and starts closed. It must not be cached across boots. +pub async fn register( + mut stream: tokio::net::UnixStream, + allocation: &Allocation, +) -> io::Result { + allocation.validate()?; + crate::write_frame(&mut stream, &crate::ToProxyd::PrivateNetwork).await?; + write(&mut stream, allocation).await?; + let reply: Reply = tokio::time::timeout( + std::time::Duration::from_secs(HANDOFF_SECONDS), + read(&mut stream), + ) + .await + .map_err(|_| invalid())??; + if reply + != (Reply { + version: VERSION, + sequence: 0, + epoch: 0, + state: "closed".into(), + }) + { + return Err(invalid()); + } + Ok(stream) +} + +/// Receive exactly one connected Unix stream, with close-on-exec set before it +/// leaves this function. Close every installed descriptor on malformed input. +#[cfg(unix)] +pub async fn receive_stream(socket: &tokio::net::UnixStream) -> io::Result { + use nix::sys::socket::{self, ControlMessageOwned, MsgFlags}; + use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; + let fd = socket + .async_io(tokio::io::Interest::READABLE, || { + let mut carrier = [0u8; 1]; + let mut iov = [io::IoSliceMut::new(&mut carrier)]; + // Receive the OS maximum, then reject every count except one. + // XNU UIPC_MAX_CMSG_FD is 512; Linux SCM_MAX_FD is 253. + // A smaller buffer can leak unreported descriptors on Darwin: + // unp_externalize installs all rights before copyout truncation. + // https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/uipc_usrreq.c + let mut control = nix::cmsg_space!([std::os::fd::RawFd; 512]); + #[cfg(target_os = "linux")] + let flags = MsgFlags::MSG_CMSG_CLOEXEC; + #[cfg(not(target_os = "linux"))] + let flags = MsgFlags::empty(); + let msg = + socket::recvmsg::<()>(socket.as_raw_fd(), &mut iov, Some(&mut control), flags) + .map_err(io::Error::from)?; + let mut fds = Vec::new(); + for message in msg.cmsgs().map_err(io::Error::from)? { + if let ControlMessageOwned::ScmRights(raw) = message { + for fd in raw { + // SAFETY: the kernel installed this descriptor. It is + // wrapped once and dropped even on a rejected message. + fds.push(unsafe { OwnedFd::from_raw_fd(fd) }); + } + } + } + let bad = msg.bytes != 1 + || msg + .flags + .intersects(MsgFlags::MSG_CTRUNC | MsgFlags::MSG_TRUNC); + if bad || carrier[0] != 0xED || fds.len() != 1 { + return Err(invalid()); + } + let fd = fds.pop().ok_or_else(invalid)?; + // SAFETY: fd is live and owned. F_SETFD changes only its descriptor flag. + if unsafe { + nix::libc::fcntl(fd.as_raw_fd(), nix::libc::F_SETFD, nix::libc::FD_CLOEXEC) + } == -1 + { + return Err(io::Error::last_os_error()); + } + if socket::getsockopt(&fd, socket::sockopt::SockType).map_err(io::Error::from)? + != socket::SockType::Stream + { + return Err(invalid()); + } + socket::getpeername::(fd.as_raw_fd()).map_err(io::Error::from)?; + Ok(fd) + }) + .await?; + let stream = std::os::unix::net::UnixStream::from(fd); + stream.set_nonblocking(true)?; + tokio::net::UnixStream::from_std(stream) +} + +/// Drain canceled headers and descriptor carriers until the helper confirms +/// cleanup by closing its reliable private stream. The caller bounds the wait. +#[cfg(unix)] +pub async fn drain_to_eof(socket: &tokio::net::UnixStream) -> io::Result<()> { + use nix::sys::socket::{self, ControlMessageOwned, MsgFlags}; + use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; + loop { + let bytes = socket + .async_io(tokio::io::Interest::READABLE, || { + let mut bytes = [0; MAX_HEADER]; + let mut iov = [io::IoSliceMut::new(&mut bytes)]; + // Keep the same OS-maximum ancillary capacity as receive_stream. + let mut control = nix::cmsg_space!([std::os::fd::RawFd; 512]); + #[cfg(target_os = "linux")] + let flags = MsgFlags::MSG_CMSG_CLOEXEC; + #[cfg(not(target_os = "linux"))] + let flags = MsgFlags::empty(); + let message = + socket::recvmsg::<()>(socket.as_raw_fd(), &mut iov, Some(&mut control), flags) + .map_err(io::Error::from)?; + for message in message.cmsgs().map_err(io::Error::from)? { + if let ControlMessageOwned::ScmRights(fds) = message { + for fd in fds { + // SAFETY: each received descriptor is newly installed + // by the kernel and is closed here exactly once. + drop(unsafe { OwnedFd::from_raw_fd(fd) }); + } + } + } + if message + .flags + .intersects(MsgFlags::MSG_CTRUNC | MsgFlags::MSG_TRUNC) + { + return Err(invalid()); + } + Ok(message.bytes) + }) + .await?; + if bytes == 0 { + return Ok(()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::AsyncWriteExt; + #[tokio::test] + async fn oversized_private_header_is_rejected_before_body_read() { + let (mut a, mut b) = tokio::net::UnixStream::pair().unwrap(); + a.write_all(&2049u32.to_be_bytes()).await.unwrap(); + let result = tokio::time::timeout( + std::time::Duration::from_millis(100), + read::<_, Request>(&mut b), + ) + .await; + assert!(matches!(result, Ok(Err(e)) if e.kind() == io::ErrorKind::InvalidData)); + } + #[tokio::test] + async fn descriptor_handoff_rejects_wrong_types_and_closes_all_extra_fds() { + use nix::sys::socket::{sendmsg, ControlMessage, MsgFlags}; + use std::os::fd::{AsFd, AsRawFd}; + let (sender, receiver) = tokio::net::UnixStream::pair().unwrap(); + let file = tempfile::tempfile().unwrap(); + let raw = sender.as_fd().try_clone_to_owned().unwrap(); + crate::send_fd(&std::os::unix::net::UnixStream::from(raw), file.as_fd()).unwrap(); + assert!(receive_stream(&receiver).await.is_err()); + let (mut a, b) = tokio::net::UnixStream::pair().unwrap(); + let (mut c, d) = tokio::net::UnixStream::pair().unwrap(); + let fds = [b.as_raw_fd(), d.as_raw_fd()]; + let control = [ControlMessage::ScmRights(&fds)]; + sendmsg::<()>( + sender.as_raw_fd(), + &[io::IoSlice::new(&[0xED])], + &control, + MsgFlags::empty(), + None, + ) + .unwrap(); + drop(b); + drop(d); + assert!(receive_stream(&receiver).await.is_err()); + let mut byte = [0]; + assert_eq!(a.read(&mut byte).await.unwrap(), 0); + assert_eq!(c.read(&mut byte).await.unwrap(), 0); + } + #[tokio::test] + async fn oversized_rights_message_closes_every_installed_descriptor() { + use nix::sys::socket::{sendmsg, ControlMessage, MsgFlags}; + use std::os::fd::AsRawFd; + let (sender, receiver) = tokio::net::UnixStream::pair().unwrap(); + let mut peers = Vec::new(); + let mut passed = Vec::new(); + for _ in 0..32 { + let (a, b) = tokio::net::UnixStream::pair().unwrap(); + peers.push(a); + passed.push(b); + } + let fds: Vec<_> = passed.iter().map(AsRawFd::as_raw_fd).collect(); + sendmsg::<()>( + sender.as_raw_fd(), + &[io::IoSlice::new(&[0xED])], + &[ControlMessage::ScmRights(&fds)], + MsgFlags::empty(), + None, + ) + .unwrap(); + drop(passed); + assert!(receive_stream(&receiver).await.is_err()); + for (index, mut peer) in peers.into_iter().enumerate() { + let mut byte = [0]; + assert!( + matches!( + tokio::time::timeout( + std::time::Duration::from_millis(50), + peer.read(&mut byte) + ) + .await, + Ok(Ok(0)) + ), + "oversized SCM_RIGHTS must not leak an installed FD at {index}" + ); + } + } + #[tokio::test] + async fn cleanup_drain_closes_pending_rights_and_requires_reliable_eof() { + use std::os::fd::AsFd; + let (mut sender, receiver) = tokio::net::UnixStream::pair().unwrap(); + let (mut flow, passed) = tokio::net::UnixStream::pair().unwrap(); + sender.write_all(b"partial header").await.unwrap(); + let raw = sender.as_fd().try_clone_to_owned().unwrap(); + crate::send_fd(&std::os::unix::net::UnixStream::from(raw), passed.as_fd()).unwrap(); + drop(passed); + let drain = tokio::spawn(async move { drain_to_eof(&receiver).await }); + let mut byte = [0]; + assert_eq!( + tokio::time::timeout(std::time::Duration::from_secs(1), flow.read(&mut byte)) + .await + .unwrap() + .unwrap(), + 0 + ); + assert!( + !drain.is_finished(), + "discarded bytes are not cleanup acknowledgement" + ); + drop(sender); + drain.await.unwrap().unwrap(); + } + #[test] + fn valid_bind_decodes() { + serde_json::from_str::( + r#"{"version":1,"sequence":1,"epoch":7,"operation":{"kind":"bind"}}"#, + ) + .unwrap(); + } + #[tokio::test] + async fn required_port_and_unknown_guest_identity_are_rejected() { + for body in [ + r#"{"version":1,"sequence":1,"epoch":1,"operation":{"kind":"tcp","destination":"192.0.2.9"}}"#, + r#"{"version":1,"sequence":1,"epoch":1,"operation":{"kind":"tcp","destination":"192.0.2.9","port":443,"session_id":"forged"}}"#, + ] { + let mut bytes = (body.len() as u32).to_be_bytes().to_vec(); + bytes.extend_from_slice(body.as_bytes()); + assert!(read::<_, Request>(&mut bytes.as_slice()).await.is_err()); + } + } +} diff --git a/crates/engram-egress-proxy/src/connections.rs b/crates/engram-egress-proxy/src/connections.rs index 2da60ac47..19973dcd2 100644 --- a/crates/engram-egress-proxy/src/connections.rs +++ b/crates/engram-egress-proxy/src/connections.rs @@ -10,6 +10,7 @@ pub(crate) struct Connections { } struct State { open: bool, + poisoned: bool, active: usize, cancel: watch::Sender, } @@ -18,6 +19,7 @@ impl Default for Connections { Self { state: Mutex::new(State { open: true, + poisoned: false, active: 0, cancel: watch::channel(0).0, }), @@ -57,8 +59,14 @@ impl Connections { notified.await; } } + pub(crate) fn is_poisoned(&self) -> bool { + self.state.lock().poisoned + } pub(crate) fn open(&self) { - self.state.lock().open = true; + let mut state = self.state.lock(); + if !state.poisoned { + state.open = true; + } } } @@ -67,6 +75,21 @@ pub struct ConnectionLease { cancel: watch::Receiver, } impl ConnectionLease { + /// A failed private cleanup barrier cannot be repaired by policy replay + /// in this proxy incarnation. Retain the closed session gate. + pub fn poison(&self) { + let mut state = self.control.state.lock(); + state.poisoned = true; + state.open = false; + state.cancel.send_modify(|v| *v = v.wrapping_add(1)); + } + + /// Allocation owners observe cancellation, close admission, then join all + /// child work before dropping this lease and releasing a policy drain. + pub async fn cancelled(&mut self) { + let _ = self.cancel.changed().await; + } + /// Drop the complete socket future before releasing the drain guard. pub async fn run(mut self, future: F) -> Option { tokio::select! { biased; diff --git a/crates/engram-egress-proxy/src/dns.rs b/crates/engram-egress-proxy/src/dns.rs index 7e347f15b..4c1ed1242 100644 --- a/crates/engram-egress-proxy/src/dns.rs +++ b/crates/engram-egress-proxy/src/dns.rs @@ -391,6 +391,43 @@ fn build_nxdomain(query: &Message) -> Message { resp } +/// Bounded private DNS exchange. Uses the ordinary name policy and codec; +/// no detached demux/reaper can deliver after the allocation lease ends. +pub async fn resolve_private( + bytes: &[u8], + state: &SessionState, + upstream: SocketAddr, +) -> std::io::Result> { + if bytes.len() > MAX_DGRAM { + return Err(std::io::Error::other("DNS query too large")); + } + let query = Message::from_bytes(bytes).map_err(std::io::Error::other)?; + if query.message_type() != MessageType::Query || query.queries().len() != 1 { + return Err(std::io::Error::other("invalid DNS query")); + } + let name = query.queries()[0] + .name() + .to_utf8() + .trim_end_matches('.') + .to_ascii_lowercase(); + if !name_allowed(state, &name) { + return build_nxdomain(&query) + .to_vec() + .map_err(std::io::Error::other); + } + let response = tokio::time::timeout(TCP_UPSTREAM_TIMEOUT, forward_tcp(bytes, upstream)) + .await + .map_err(|_| std::io::Error::other("DNS exchange timeout"))??; + let parsed = Message::from_bytes(&response).map_err(std::io::Error::other)?; + if parsed.id() != query.id() + || parsed.message_type() != MessageType::Response + || parsed.queries() != query.queries() + { + return Err(std::io::Error::other("invalid DNS reply")); + } + Ok(response) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/engram-egress-proxy/src/proxy.rs b/crates/engram-egress-proxy/src/proxy.rs index 9a0c0df79..fd1d43385 100644 --- a/crates/engram-egress-proxy/src/proxy.rs +++ b/crates/engram-egress-proxy/src/proxy.rs @@ -37,6 +37,7 @@ const SNI_PEEK_BUDGET: Duration = Duration::from_secs(5); /// Max bytes we'll buffer of the ClientHello. const SNI_PEEK_BYTES: usize = 16 * 1024; +#[derive(Clone)] pub struct ProxyConfig { /// Where to listen for REDIRECTed traffic. Typically /// `0.0.0.0:9443` with iptables redirecting VM TAP traffic to @@ -112,6 +113,7 @@ impl ProxyConfig { } } +#[derive(Clone)] pub struct Proxy { cfg: ProxyConfig, server_cfg: Arc, @@ -132,6 +134,42 @@ impl Proxy { } } + /// Dispatch a host-private stream with its mandatory original destination. + /// The caller must own a registry lease until this future has been dropped. + pub async fn dispatch_private( + &self, + stream: tokio::net::UnixStream, + session: Arc, + destination: std::net::SocketAddrV4, + ) -> std::io::Result<()> { + if destination.port() == 0 { + return Err(std::io::Error::other("missing original destination port")); + } + handle( + Some((IpAddr::V4(*destination.ip()), destination.port())), + stream, + session, + self.cfg.resolver.clone(), + self.server_cfg.clone(), + self.client_cfg.clone(), + self.cfg.observe_sink.clone(), + self.cfg.inject_refresher.clone(), + self.cfg.guest_port_dialer.clone(), + ) + .await + .map_err(std::io::Error::other) + } + + /// One controlled DNS request. The allocation owner holds the same lease + /// as TCP and owns cancellation through response delivery. + pub async fn resolve_private( + &self, + query: &[u8], + session: &crate::SessionState, + ) -> std::io::Result> { + dns::resolve_private(query, session, self.cfg.dns_upstream).await + } + /// Bind the 443 listener and (when configured) the DNS sockets, /// returning just the sockets for [`serve`](Self::serve) to run. /// @@ -233,8 +271,8 @@ impl Proxy { tokio::spawn(async move { if let Some(Err(e)) = lease .run(handle( + original_destination(&stream).ok(), stream, - peer, session, resolver, server_cfg, @@ -262,9 +300,9 @@ pub struct Listeners { } #[allow(clippy::too_many_arguments)] -async fn handle( - mut stream: TcpStream, - _peer: SocketAddr, +async fn handle( + original_dst: Option<(IpAddr, u16)>, + mut stream: C, session: Arc, resolver: Arc, server_cfg: Arc, @@ -277,7 +315,6 @@ async fn handle( // dial it; the upstream is resolved fresh by SNI on the host. // On non-Linux this errors (no SO_ORIGINAL_DST) — fine, we // just log "unknown". - let original_dst = original_destination(&stream).ok(); let (sni, peeked) = sni::peek_sni(&mut stream, SNI_PEEK_BYTES, SNI_PEEK_BUDGET) .await diff --git a/crates/engram-egress-proxy/src/registry.rs b/crates/engram-egress-proxy/src/registry.rs index 083179f1a..90e379dee 100644 --- a/crates/engram-egress-proxy/src/registry.rs +++ b/crates/engram-egress-proxy/src/registry.rs @@ -532,6 +532,22 @@ impl Registry { Some((state, lease)) } + /// Trusted host identity only. Enroll before cloning the policy snapshot. + /// Holding this lease makes replacement wait for allocation cleanup. + pub fn enroll_session( + &self, + session: SessionId, + ) -> Option<(Arc, crate::connections::ConnectionLease)> { + let g = self.inner.read(); + let lease = g.controls.get(&session)?.enroll()?; + let ip = g.by_session.get(&session)?; + let state = g.by_guest_ip.get(ip)?; + if state.session_id != session { + return None; + } + Some((state.clone(), lease)) + } + pub async fn apply(&self, state: SessionState) -> Result<(), String> { if let Some(policy) = &state.managed_deny { policy.validate()?; @@ -573,6 +589,9 @@ impl Registry { control.close(); control.drain().await; } + if control.is_poisoned() { + return Err("private network cleanup failed; replace proxy before replay".into()); + } Self::publish(&mut self.inner.write(), state); control.open(); Ok(()) @@ -590,6 +609,13 @@ impl Registry { control.close(); control.drain().await; } + pub fn is_poisoned(&self, session_id: SessionId) -> bool { + self.inner + .read() + .controls + .get(&session_id) + .is_some_and(|control| control.is_poisoned()) + } pub fn lookup_session(&self, session_id: SessionId) -> Option> { let g = self.inner.read(); let ip = g.by_session.get(&session_id)?; diff --git a/crates/engram-egress-proxyd/src/control.rs b/crates/engram-egress-proxyd/src/control.rs index d9d1fce12..6836ce3e5 100644 --- a/crates/engram-egress-proxyd/src/control.rs +++ b/crates/engram-egress-proxyd/src/control.rs @@ -17,6 +17,9 @@ use engram_egress_proxy::{GuestGatewayRegistry, Listeners, Proxy, Registry}; pub(crate) struct ControlState { pub registry: Arc, + pub private: Arc, + pub proxy: Arc, + pub host_id: engram_core::HostId, pub gateway: Arc, pub hello: HelloInfo, /// Flipped by the `Shutdown` op; [`crate::run`] selects on it and @@ -57,10 +60,31 @@ async fn serve_conn( loop { let msg: ToProxyd = engram_egress_proto::read_frame(&mut stream).await?; let reply = match msg { + ToProxyd::PrivateNetwork => { + let allocation = engram_egress_proto::private::read(&mut stream).await?; + let proxy = state.proxy.clone(); + return state + .private + .serve( + stream, + allocation, + state.registry.clone(), + proxy, + state.host_id, + &state.hello.incarnation, + ) + .await; + } ToProxyd::ListManaged => FromProxyd::ManagedSessions(state.registry.managed_sessions()), ToProxyd::QuarantineManaged(session) => { state.registry.quarantine_and_drain(session).await; - FromProxyd::Ok + if state.registry.is_poisoned(session) { + FromProxyd::Err( + "private network cleanup failed; replace proxy before replay".into(), + ) + } else { + FromProxyd::Ok + } } ToProxyd::ReadManaged(session) => { FromProxyd::ManagedState(state.registry.lookup_session(session).and_then(|s| { @@ -96,8 +120,10 @@ async fn serve_conn( } } ToProxyd::RemoveSession(session_id) => { - unregister_session(&state, session_id); - FromProxyd::Ok + match unregister_session(&state, session_id).await { + Ok(()) => FromProxyd::Ok, + Err(error) => FromProxyd::Err(error), + } } ToProxyd::Health => FromProxyd::HealthReport { sessions: state.registry.live_count(), @@ -122,6 +148,7 @@ async fn serve_conn( })) } ToProxyd::Shutdown => { + state.private.shutdown().await; tracing::info!("shutdown requested over control socket; exiting"); let _ = engram_egress_proto::write_frame(&mut stream, &FromProxyd::Ok).await; let _ = state.shutdown.send(true); @@ -135,9 +162,17 @@ async fn serve_conn( /// The single session-teardown entry point (the `HostEgress:: /// unregister_session` contract): drop the registration AND fan /// `session_closed` to every tunnel upstream, or pooled state leaks. -fn unregister_session(state: &ControlState, session_id: engram_core::SessionId) { +async fn unregister_session( + state: &ControlState, + session_id: engram_core::SessionId, +) -> Result<(), String> { + state.registry.quarantine_and_drain(session_id).await; + if state.registry.is_poisoned(session_id) { + return Err("private network cleanup failed; replace proxy before replay".into()); + } state.registry.unregister(session_id); state.gateway.session_closed(session_id); + Ok(()) } /// Full replace: register everything in the set, then drop any @@ -170,7 +205,7 @@ async fn sync_policies( Ok(()) => applied += 1, Err(e) => { failed += 1; - managed_failed |= managed; + managed_failed |= managed || state.registry.is_poisoned(session_id); tracing::error!( %session_id, error = %e, @@ -187,8 +222,11 @@ async fn sync_policies( .into_iter() .filter(|sid| !keep.contains(sid)) { - unregister_session(state, stale); - pruned += 1; + if unregister_session(state, stale).await.is_err() { + managed_failed = true; + } else { + pruned += 1; + } } tracing::info!(applied, failed, pruned, total, "policies synced"); if managed_failed { @@ -413,8 +451,19 @@ mod tests { } fn state() -> ControlState { + let registry = Arc::new(Registry::new()); + let dir = tempfile::tempdir().unwrap(); + let ca = Arc::new(engram_egress_proxy::Ca::load_or_generate(dir.path()).unwrap()); + let proxy = Arc::new(Proxy::new(engram_egress_proxy::ProxyConfig::new( + "127.0.0.1:0".parse().unwrap(), + registry.clone(), + Arc::new(engram_egress_proxy::CertMint::new(ca)), + ))); ControlState { - registry: Arc::new(Registry::new()), + private: Arc::new(crate::private::Ingress::default()), + proxy, + host_id: engram_core::HostId::new(), + registry, gateway: Arc::new(GuestGatewayRegistry::default()), shutdown: tokio::sync::watch::channel(false).0, hello: HelloInfo { @@ -430,6 +479,36 @@ mod tests { } } + #[tokio::test] + async fn sync_cannot_acknowledge_a_poisoned_session_as_ordinary_or_removed() { + let state = state(); + let session = SessionId::new(); + let mut p = policy(session, "192.0.2.2".parse().unwrap()); + p.managed_deny = Some( + engram_core::types::managed_deny::ManagedDenyPolicy::new(1, ["mcp.example".into()]) + .unwrap(), + ); + register_policy(&state.registry, p.clone()).await.unwrap(); + let (_, lease) = state.registry.enroll_session(session).unwrap(); + lease.poison(); + drop(lease); + assert!(matches!( + sync_policies(&state, vec![p.clone()]).await, + FromProxyd::Err(_) + )); + p.managed_deny = None; + assert!(matches!( + sync_policies(&state, vec![p]).await, + FromProxyd::Err(_) + )); + assert!(matches!( + sync_policies(&state, vec![]).await, + FromProxyd::Err(_) + )); + assert!(unregister_session(&state, session).await.is_err()); + assert!(state.registry.is_poisoned(session)); + } + #[tokio::test] async fn sync_registers_and_prunes() { let state = state(); @@ -547,4 +626,69 @@ mod tests { "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" ); } + #[tokio::test] + async fn private_upgrade_uses_dedicated_host_uds_and_never_returns_final_receipt() { + use engram_egress_proto::private::{ + self as wire, Allocation, Operation, Reply, Request, VERSION, + }; + let state = Arc::new(state()); + let session = SessionId::new(); + let mut p = policy(session, "192.0.2.2".parse().unwrap()); + let managed = + engram_core::types::managed_deny::ManagedDenyPolicy::new(1, ["mcp.example".into()]) + .unwrap(); + p.managed_deny = Some(managed.clone()); + let a = Allocation { + version: VERSION, + host_id: state.host_id, + host_incarnation: "host-1".into(), + proxy_incarnation: state.hello.incarnation.clone(), + session_id: session, + sandbox_id: p.sandbox_id, + boot_attempt: "boot-1".into(), + attachment_generation: 1, + guest_ip: p.guest_ip, + gateway_ip: "192.0.2.1".parse().unwrap(), + policy_revision: 1, + policy_digest: managed.digest, + }; + register_policy(&state.registry, p).await.unwrap(); + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("private.sock"); + let listener = tokio::net::UnixListener::bind(&path).unwrap(); + let task = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + serve_conn(stream, state).await + }); + let client = tokio::net::UnixStream::connect(path).await.unwrap(); + let mut capability = wire::register(client, &a).await.unwrap(); + wire::write( + &mut capability, + &Request { + version: VERSION, + sequence: 1, + epoch: 7, + operation: Operation::Bind, + }, + ) + .await + .unwrap(); + let ready: Reply = wire::read(&mut capability).await.unwrap(); + assert_eq!(ready.state, "ready"); + wire::write( + &mut capability, + &Request { + version: VERSION, + sequence: 2, + epoch: 7, + operation: Operation::Close, + }, + ) + .await + .unwrap(); + let closed: Reply = wire::read(&mut capability).await.unwrap(); + assert_eq!(closed.state, "closed"); + drop(capability); + task.await.unwrap().unwrap(); + } } diff --git a/crates/engram-egress-proxyd/src/lib.rs b/crates/engram-egress-proxyd/src/lib.rs index 82cc09f33..2932136a8 100644 --- a/crates/engram-egress-proxyd/src/lib.rs +++ b/crates/engram-egress-proxyd/src/lib.rs @@ -16,6 +16,7 @@ mod control; mod coord; mod dialback; +mod private; use std::path::PathBuf; use std::sync::Arc; @@ -181,6 +182,17 @@ pub async fn run(args: DaemonArgs) -> i32 { } }; + // Registration grants a live descriptor capability. Only the daemon owner + // can use this host-local control socket. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if std::fs::set_permissions(&control_sock, std::fs::Permissions::from_mode(0o600)).is_err() + { + return 1; + } + } + let listeners = match control::bind_with_retry(&proxy).await { Ok(l) => l, Err(e) => { @@ -228,7 +240,11 @@ pub async fn run(args: DaemonArgs) -> i32 { ); let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false); + let private = Arc::new(private::Ingress::default()); let control_state = Arc::new(control::ControlState { + private: private.clone(), + proxy: Arc::new(proxy.clone()), + host_id: args.host_id, registry, gateway: guest_gateway, hello, @@ -236,7 +252,7 @@ pub async fn run(args: DaemonArgs) -> i32 { }); tokio::spawn(control::serve(control_listener, control_state)); - tokio::select! { + let exit = tokio::select! { _ = proxy.serve(listeners) => { // `serve` loops forever on accept. If it returns, the data // plane is dead while iptables still REDIRECTs here: exit @@ -250,7 +266,9 @@ pub async fn run(args: DaemonArgs) -> i32 { tracing::info!("graceful shutdown (control-socket Shutdown op)"); 0 } - } + }; + private.shutdown().await; + exit } /// cgroup-v2 self-migration (the ADR 0044 K2 escape, self-service diff --git a/crates/engram-egress-proxyd/src/private.rs b/crates/engram-egress-proxyd/src/private.rs new file mode 100644 index 000000000..a242a87f8 --- /dev/null +++ b/crates/engram-egress-proxyd/src/private.rs @@ -0,0 +1,893 @@ +//! Allocation-owned private ingress. No guest listener or general proxy API. +use engram_core::{HostId, SessionId}; +use engram_egress_proto::private::{self as wire, Allocation}; +use engram_egress_proxy::{Proxy, Registry}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; +use tokio::{ + net::UnixStream, + sync::{watch, Mutex}, +}; + +type Slot = Arc>; +#[derive(Default)] +struct SlotState { + live: Option, + last: Option<(String, u64)>, + incarnations: HashSet, +} +struct Live { + cancel: watch::Sender, + done: watch::Receiver, +} +#[derive(Default)] +pub(crate) struct Ingress { + stopped: std::sync::atomic::AtomicBool, + slots: Mutex>, + used: Mutex>, +} +impl Ingress { + pub(crate) async fn shutdown(&self) { + self.stopped + .store(true, std::sync::atomic::Ordering::Release); + let slots: Vec<_> = self.slots.lock().await.values().cloned().collect(); + for slot in slots { + if let Some(mut live) = slot.lock().await.live.take() { + live.cancel.send_replace(true); + while !*live.done.borrow_and_update() { + if live.done.changed().await.is_err() { + break; + } + } + } + } + } + + pub(crate) async fn serve( + &self, + mut stream: UnixStream, + allocation: Allocation, + registry: Arc, + proxy: Arc, + host: HostId, + incarnation: &str, + ) -> std::io::Result<()> { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use wire::{Operation, Reply, Request, VERSION}; + allocation.validate()?; + if self.stopped.load(std::sync::atomic::Ordering::Acquire) { + return Err(wire::invalid()); + } + if allocation.host_id != host || allocation.proxy_incarnation != incarnation { + return Err(wire::invalid()); + } + { + // Bounded replay tombstones live for this proxy incarnation. Once + // exhausted, fail closed until an operator replaces the daemon. + let mut used = self.used.lock().await; + if used.len() >= 4096 + || !used.insert(( + allocation.host_incarnation.clone(), + allocation.boot_attempt.clone(), + allocation.attachment_generation, + )) + { + return Err(wire::invalid()); + } + } + let slot = self + .slots + .lock() + .await + .entry(allocation.session_id) + .or_insert_with(|| Arc::new(Mutex::new(SlotState::default()))) + .clone(); + let mut slot = slot.lock().await; + if let Some((incarnation, generation)) = &slot.last { + if (incarnation == &allocation.host_incarnation + && allocation.attachment_generation <= *generation) + || (incarnation != &allocation.host_incarnation + && slot.incarnations.contains(&allocation.host_incarnation)) + { + return Err(wire::invalid()); + } + } + if let Some(mut old) = slot.live.take() { + old.cancel.send_replace(true); + while !*old.done.borrow_and_update() { + if old.done.changed().await.is_err() { + break; + } + } + } + // The session lease precedes all policy reads. Policy replacement + // cancels this allocation and waits for every task below to be joined. + let (session, mut lease) = registry + .enroll_session(allocation.session_id) + .ok_or_else(wire::invalid)?; + let policy = session.managed_deny.as_ref().ok_or_else(wire::invalid)?; + if session.sandbox_id != allocation.sandbox_id + || session.guest_ip != allocation.guest_ip + || policy.revision != allocation.policy_revision + || policy.digest != allocation.policy_digest + { + return Err(wire::invalid()); + } + let (cancel, mut cancelled) = watch::channel(false); + let (done, done_rx) = watch::channel(false); + if self.stopped.load(std::sync::atomic::Ordering::Acquire) { + return Err(wire::invalid()); + } + slot.last = Some(( + allocation.host_incarnation.clone(), + allocation.attachment_generation, + )); + slot.incarnations + .insert(allocation.host_incarnation.clone()); + slot.live = Some(Live { + cancel, + done: done_rx, + }); + drop(slot); + let mut tasks = tokio::task::JoinSet::new(); + let dns_budget = Arc::new(tokio::sync::Semaphore::new(wire::MAX_DNS_FLOWS)); + let operation = async { + wire::write( + &mut stream, + &Reply { + version: VERSION, + sequence: 0, + epoch: 0, + state: "closed".into(), + }, + ) + .await?; + let mut sequence = 0u64; + let mut bound_epoch = None; + loop { + // A frame read must not be canceled when an unrelated flow + // completes: it may already have consumed part of the header. + let request: Request = match wire::read(&mut stream).await { + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), + other => other?, + }; + while tasks.try_join_next().is_some() {} + if request.version != VERSION + || sequence.checked_add(1) != Some(request.sequence) + || request.epoch == 0 + || request.epoch > 9_007_199_254_740_991 + { + return Err(wire::invalid()); + } + sequence = request.sequence; + let state = match request.operation { + Operation::Bind if bound_epoch.is_none() => { + bound_epoch = Some(request.epoch); + "ready" + } + Operation::Close if bound_epoch == Some(request.epoch) => { + return Ok(Some(Reply { + version: VERSION, + sequence, + epoch: request.epoch, + state: "closed".into(), + })) + } + Operation::Tcp { destination, port } | Operation::Dns { destination, port } => { + if bound_epoch != Some(request.epoch) + || port == 0 + || destination.is_unspecified() + || destination.is_multicast() + || tasks.len() >= wire::MAX_FLOWS + { + return Err(wire::invalid()); + } + let dns = matches!(request.operation, Operation::Dns { .. }); + if dns && (destination != allocation.gateway_ip || port != 53) { + return Err(wire::invalid()); + } + // Gateway DNS must use the controlled decoder, including TCP. + if !dns && destination == allocation.gateway_ip && port == 53 { + return Err(wire::invalid()); + } + let permit = if dns { + Some( + dns_budget + .clone() + .try_acquire_owned() + .map_err(|_| wire::invalid())?, + ) + } else { + None + }; + let mut fd = tokio::time::timeout( + std::time::Duration::from_secs(wire::HANDOFF_SECONDS), + wire::receive_stream(&stream), + ) + .await + .map_err(|_| wire::invalid())??; + let proxy = proxy.clone(); + let session = session.clone(); + tasks.spawn(async move { + let _permit = permit; + if dns { + let _ = tokio::time::timeout( + std::time::Duration::from_secs(10), + async { + let len = fd.read_u16().await? as usize; + if len == 0 || len > wire::MAX_DNS { + return Err(wire::invalid()); + } + let mut query = vec![0; len]; + fd.read_exact(&mut query).await?; + let response = + proxy.resolve_private(&query, &session).await?; + if response.len() > wire::MAX_DNS { + return Err(wire::invalid()); + } + fd.write_u16(response.len() as u16).await?; + fd.write_all(&response).await + }, + ) + .await; + } else { + let _ = proxy + .dispatch_private( + fd, + session, + std::net::SocketAddrV4::new(destination, port), + ) + .await; + } + }); + "accepted" + } + _ => return Err(wire::invalid()), + }; + wire::write( + &mut stream, + &Reply { + version: VERSION, + sequence, + epoch: request.epoch, + state: state.into(), + }, + ) + .await?; + } + }; + let result = tokio::select! { biased; + _ = lease.cancelled() => Ok(None), + _ = cancelled.changed() => Ok(None), + result = operation => result, + }; + // No task outlives the stable session lease. This includes TLS peeks, + // HTTP/2 pumps, DNS upstream sockets, and response writes. + tasks.shutdown().await; + let closed_reply = if let Ok(Some(reply)) = &result { + wire::write(&mut stream, reply).await + } else { + Ok(()) + }; + let shutdown = stream.shutdown().await; + // EOF on this reliable stream is the helper's cleanup barrier. The + // helper closes it only after packet admission and all work stop. + // A missing barrier must not turn a policy apply into a success ACK. + let barrier = tokio::time::timeout( + std::time::Duration::from_secs(wire::HANDOFF_SECONDS), + wire::drain_to_eof(&stream), + ) + .await; + let barrier_failed = + !matches!(barrier, Ok(Ok(()))) || shutdown.is_err() || closed_reply.is_err(); + if barrier_failed { + lease.poison(); + } + drop(stream); + drop(lease); + done.send_replace(true); + if barrier_failed { + return Err(wire::invalid()); + } + result.map(|_| ()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use engram_core::{types::managed_deny::ManagedDenyPolicy, SandboxId}; + use engram_egress_proxy::{Ca, CertMint, HostList, ProxyConfig, SessionState}; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use wire::{Operation, Reply, Request, VERSION}; + struct Fixture { + ingress: Arc, + registry: Arc, + proxy: Arc, + allocation: Allocation, + ca: Arc, + } + impl Fixture { + async fn new() -> Self { + let _ = rustls::crypto::ring::default_provider().install_default(); + let registry = Arc::new(Registry::new()); + let policy = ManagedDenyPolicy::new(1, ["mcp.example".into()]).unwrap(); + let allocation = Allocation { + version: VERSION, + host_id: HostId::new(), + host_incarnation: "host-1".into(), + proxy_incarnation: "proxy-1".into(), + session_id: SessionId::new(), + sandbox_id: SandboxId::new(), + boot_attempt: "boot-1".into(), + attachment_generation: 1, + guest_ip: "192.0.2.2".parse().unwrap(), + gateway_ip: "192.0.2.1".parse().unwrap(), + policy_revision: 1, + policy_digest: policy.digest.clone(), + }; + registry + .apply(SessionState { + managed_deny: Some(policy), + session_id: allocation.session_id, + sandbox_id: allocation.sandbox_id, + guest_ip: allocation.guest_ip, + network_allow: HostList::from_manifest(&[], &[]).unwrap(), + allow_all: true, + secrets: vec![], + injects: vec![], + observes: vec![], + guest_services: vec![], + tunnels: vec![], + apps: vec![], + }) + .await + .unwrap(); + let tmp = tempfile::tempdir().unwrap(); + let ca = Arc::new(Ca::load_or_generate(tmp.path()).unwrap()); + let proxy = Arc::new(Proxy::new(ProxyConfig::new( + "127.0.0.1:0".parse().unwrap(), + registry.clone(), + Arc::new(CertMint::new(ca.clone())), + ))); + Self { + ingress: Arc::new(Ingress::default()), + registry, + proxy, + allocation, + ca, + } + } + fn start( + &self, + a: Allocation, + ) -> (UnixStream, tokio::task::JoinHandle>) { + let (client, server) = UnixStream::pair().unwrap(); + let ingress = self.ingress.clone(); + let reg = self.registry.clone(); + let proxy = self.proxy.clone(); + let host = self.allocation.host_id; + ( + client, + tokio::spawn( + async move { ingress.serve(server, a, reg, proxy, host, "proxy-1").await }, + ), + ) + } + } + async fn reply(c: &mut UnixStream) -> Reply { + tokio::time::timeout(std::time::Duration::from_secs(2), wire::read(c)) + .await + .unwrap() + .unwrap() + } + async fn request(c: &mut UnixStream, seq: u64, op: Operation) { + wire::write( + c, + &Request { + version: VERSION, + sequence: seq, + epoch: 7, + operation: op, + }, + ) + .await + .unwrap(); + } + async fn handoff(c: &UnixStream) -> UnixStream { + use std::os::fd::AsFd; + let (a, b) = std::os::unix::net::UnixStream::pair().unwrap(); + let control = c.as_fd().try_clone_to_owned().unwrap(); + engram_egress_proto::send_fd(&std::os::unix::net::UnixStream::from(control), b.as_fd()) + .unwrap(); + a.set_nonblocking(true).unwrap(); + UnixStream::from_std(a).unwrap() + } + #[tokio::test] + async fn private_replacement_drains_pending_tls_before_ack_and_rejects_replay() { + let f = Fixture::new().await; + let (mut c, task) = f.start(f.allocation.clone()); + assert_eq!(reply(&mut c).await.state, "closed"); + request(&mut c, 1, Operation::Bind).await; + assert_eq!(reply(&mut c).await.state, "ready"); + request( + &mut c, + 2, + Operation::Tcp { + destination: "198.51.100.7".parse().unwrap(), + port: 8443, + }, + ) + .await; + let mut flow = handoff(&c).await; + assert_eq!(reply(&mut c).await.state, "accepted"); + flow.write_all(&[22, 3]).await.unwrap(); + let mut next = f.allocation.clone(); + next.attachment_generation = 2; + let closed = tokio::spawn(async move { + assert!(wire::read::<_, Reply>(&mut c).await.is_err()); + drop(c); + }); + let (mut new, next_task) = f.start(next); + assert_eq!(reply(&mut new).await.state, "closed"); + let mut b = [0]; + assert_eq!(flow.read(&mut b).await.unwrap(), 0); + closed.await.unwrap(); + assert!(task.await.unwrap().is_ok()); + let (mut replay, old) = f.start(f.allocation.clone()); + assert!(wire::read::<_, Reply>(&mut replay).await.is_err()); + assert!(old.await.unwrap().is_err()); + drop(new); + next_task.await.unwrap().unwrap(); + } + #[tokio::test] + async fn completed_flow_does_not_cancel_a_partial_control_header() { + let f = Fixture::new().await; + let (mut c, task) = f.start(f.allocation.clone()); + reply(&mut c).await; + request(&mut c, 1, Operation::Bind).await; + reply(&mut c).await; + request( + &mut c, + 2, + Operation::Tcp { + destination: "198.51.100.7".parse().unwrap(), + port: 80, + }, + ) + .await; + let mut flow = handoff(&c).await; + reply(&mut c).await; + let body = serde_json::to_vec(&Request { + version: VERSION, + sequence: 3, + epoch: 7, + operation: Operation::Close, + }) + .unwrap(); + let frame = [(body.len() as u32).to_be_bytes().as_slice(), &body].concat(); + c.write_all(&frame[..2]).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + flow.write_all(b"GET / HTTP/1.1\r\nHost: api.example\r\n\r\n") + .await + .unwrap(); + flow.shutdown().await.unwrap(); + let mut byte = [0]; + assert!(matches!(flow.read(&mut byte).await, Ok(0) | Err(_))); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + c.write_all(&frame[2..]).await.unwrap(); + assert_eq!(reply(&mut c).await.state, "closed"); + drop(c); + task.await.unwrap().unwrap(); + } + #[tokio::test] + async fn policy_apply_waits_for_helper_cleanup_eof() { + let f = Fixture::new().await; + let state = (*f.registry.lookup_session(f.allocation.session_id).unwrap()).clone(); + let (mut c, task) = f.start(f.allocation.clone()); + reply(&mut c).await; + let registry = f.registry.clone(); + let apply = tokio::spawn(async move { registry.apply(state).await }); + let mut byte = [0]; + assert_eq!(c.read(&mut byte).await.unwrap(), 0); + tokio::time::sleep(std::time::Duration::from_millis(30)).await; + assert!( + !apply.is_finished(), + "write-half EOF is a stop request, not the helper cleanup barrier" + ); + assert!(f.registry.lookup_session(f.allocation.session_id).is_none()); + drop(c); + task.await.unwrap().unwrap(); + apply.await.unwrap().unwrap(); + assert!(f.registry.lookup_session(f.allocation.session_id).is_some()); + } + + #[tokio::test] + async fn missing_helper_barrier_poison_is_session_local_and_cannot_be_replayed() { + let f = Fixture::new().await; + let state = (*f.registry.lookup_session(f.allocation.session_id).unwrap()).clone(); + let (mut c, task) = f.start(f.allocation.clone()); + reply(&mut c).await; + let registry = f.registry.clone(); + let old = state.clone(); + let apply = tokio::spawn(async move { registry.apply(old).await }); + let mut byte = [0]; + assert_eq!(c.read(&mut byte).await.unwrap(), 0); + // Keep the helper write half open. Another session must remain usable + // during this allocation's bounded cleanup wait. + let mut other = state.clone(); + other.session_id = SessionId::new(); + other.sandbox_id = SandboxId::new(); + other.guest_ip = "192.0.2.8".parse().unwrap(); + f.registry.apply(other.clone()).await.unwrap(); + assert!(f.registry.enroll_session(other.session_id).is_some()); + assert!( + tokio::time::timeout(std::time::Duration::from_secs(6), apply) + .await + .unwrap() + .unwrap() + .is_err() + ); + assert!(task.await.unwrap().is_err()); + assert!(f.registry.is_poisoned(state.session_id)); + f.registry.unregister(state.session_id); + assert!(f.registry.apply(state.clone()).await.is_err()); + assert!(f.registry.enroll_session(state.session_id).is_none()); + let mut next = f.allocation.clone(); + next.attachment_generation += 1; + let (mut rejected, next_task) = f.start(next); + assert!(wire::read::<_, Reply>(&mut rejected).await.is_err()); + assert!(next_task.await.unwrap().is_err()); + assert!(f.registry.enroll_session(other.session_id).is_some()); + drop(c); + } + #[tokio::test] + async fn policy_quarantine_joins_allocation_and_dns_without_global_block() { + let f = Fixture::new().await; + let (mut c, task) = f.start(f.allocation.clone()); + reply(&mut c).await; + request(&mut c, 1, Operation::Bind).await; + reply(&mut c).await; + request( + &mut c, + 2, + Operation::Dns { + destination: f.allocation.gateway_ip, + port: 53, + }, + ) + .await; + let mut flow = handoff(&c).await; + reply(&mut c).await; + flow.write_all(&[0, 128, 0]).await.unwrap(); + let closed = tokio::spawn(async move { + assert!(wire::read::<_, Reply>(&mut c).await.is_err()); + drop(c); + }); + tokio::time::timeout( + std::time::Duration::from_secs(2), + f.registry.quarantine_and_drain(f.allocation.session_id), + ) + .await + .unwrap(); + closed.await.unwrap(); + let mut b = [0]; + assert_eq!(flow.read(&mut b).await.unwrap(), 0); + task.await.unwrap().unwrap(); + } + #[tokio::test] + async fn wrong_identity_epoch_replay_and_non_tls_fail_closed() { + let f = Fixture::new().await; + for kind in 0..5 { + let mut bad = f.allocation.clone(); + bad.attachment_generation += kind + 1; + match kind { + 0 => bad.host_id = HostId::new(), + 1 => bad.proxy_incarnation = "proxy-old".into(), + 2 => bad.sandbox_id = SandboxId::new(), + 3 => bad.policy_revision += 1, + _ => bad.session_id = SessionId::new(), + } + let (mut c, task) = f.start(bad); + assert!(wire::read::<_, Reply>(&mut c).await.is_err()); + assert!(task.await.unwrap().is_err()); + } + let mut a = f.allocation.clone(); + a.attachment_generation = 10; + let (mut c, task) = f.start(a); + reply(&mut c).await; + request(&mut c, 1, Operation::Bind).await; + reply(&mut c).await; + request( + &mut c, + 2, + Operation::Tcp { + destination: "198.51.100.7".parse().unwrap(), + port: 80, + }, + ) + .await; + let mut flow = handoff(&c).await; + reply(&mut c).await; + flow.write_all(b"GET / HTTP/1.1\r\nHost: mcp.example\r\n\r\n") + .await + .unwrap(); + let mut bytes = [0; 64]; + assert!(matches!(flow.read(&mut bytes).await, Ok(0) | Err(_))); + request(&mut c, 2, Operation::Bind).await; + assert!(wire::read::<_, Reply>(&mut c).await.is_err()); + drop(c); + assert!(task.await.unwrap().is_err()); + } + #[tokio::test] + async fn private_tls_uses_deny_first_and_original_port_authority_without_credentials() { + let exercise = async { + for port in [443, 8443] { + for (host, authority, allowed) in [ + ("mcp.example", "mcp.example", false), + ("api.example", "mcp.example", false), + ("api.example", "api.example", true), + ] { + let mut f = Fixture::new().await; + let key = rcgen::KeyPair::generate().unwrap(); + let cert = rcgen::CertificateParams::new(vec!["api.example".into()]) + .unwrap() + .self_signed(&key) + .unwrap(); + let mut roots = rustls::RootCertStore::empty(); + roots.add(cert.der().clone()).unwrap(); + let server = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + vec![cert.der().clone()], + rustls::pki_types::PrivateKeyDer::try_from(key.serialize_der()) + .unwrap(), + ) + .unwrap(); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let mut cfg = ProxyConfig::new( + "127.0.0.1:0".parse().unwrap(), + f.registry.clone(), + Arc::new(CertMint::new(f.ca.clone())), + ); + cfg.upstream_test_roots = Some(roots); + cfg.resolver = Arc::new( + engram_egress_proxy::StaticResolver::new() + .with("api.example", listener.local_addr().unwrap()), + ); + f.proxy = Arc::new(Proxy::new(cfg)); + let (captured, mut received) = tokio::sync::mpsc::channel(1); + let upstream = tokio::spawn(async move { + let (socket, _) = listener.accept().await.unwrap(); + let mut tls = tokio_rustls::TlsAcceptor::from(Arc::new(server)) + .accept(socket) + .await + .unwrap(); + let mut bytes = vec![]; + let mut byte = [0]; + while tls.read(&mut byte).await.unwrap_or(0) > 0 { + bytes.push(byte[0]); + if bytes.ends_with(b"\r\n\r\n") { + break; + } + } + captured.send(bytes).await.unwrap(); + let _ = tls.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok").await; + let _ = tls.shutdown().await; + }); + let (mut control, task) = f.start(f.allocation.clone()); + reply(&mut control).await; + request(&mut control, 1, Operation::Bind).await; + reply(&mut control).await; + request( + &mut control, + 2, + Operation::Tcp { + destination: "198.51.100.7".parse().unwrap(), + port, + }, + ) + .await; + let flow = handoff(&control).await; + reply(&mut control).await; + let mut trust = rustls::RootCertStore::empty(); + for der in rustls_pemfile::certs(&mut f.ca.cert_pem.as_bytes()) { + trust.add(der.unwrap()).unwrap(); + } + let connector = tokio_rustls::TlsConnector::from(Arc::new( + rustls::ClientConfig::builder() + .with_root_certificates(trust) + .with_no_client_auth(), + )); + let guest = connector + .connect(host.to_string().try_into().unwrap(), flow) + .await; + if host == "mcp.example" { + assert!(guest.is_err()); + } else { + let mut guest = guest.unwrap(); + let effective = if port == 443 { + authority.to_string() + } else { + format!("{authority}:{port}") + }; + guest.write_all(format!("GET / HTTP/1.1\r\nHost: {effective}\r\nConnection: close\r\n\r\n").as_bytes()).await.unwrap(); + let mut out = vec![]; + let _ = guest.read_to_end(&mut out).await; + assert_eq!(out.starts_with(b"HTTP/1.1 200"), allowed); + let bytes = received.recv().await.unwrap(); + if allowed { + let text = String::from_utf8(bytes).unwrap(); + assert!(text.contains(&format!("Host: {effective}\r\n"))); + assert!(!text.to_lowercase().contains("authorization")); + } else { + assert!(bytes.is_empty()); + } + } + drop(control); + task.await.unwrap().unwrap(); + upstream.abort(); + let _ = upstream.await; + } + } + }; + tokio::time::timeout(std::time::Duration::from_secs(10), exercise) + .await + .expect("bounded real private TLS fixture"); + } + + #[tokio::test] + async fn unused_older_attachment_generation_cannot_replace_newer() { + let f = Fixture::new().await; + let mut newer = f.allocation.clone(); + newer.attachment_generation = 5; + let (mut current, task) = f.start(newer); + reply(&mut current).await; + let (mut older, replay) = f.start(f.allocation.clone()); + assert!(wire::read::<_, Reply>(&mut older).await.is_err()); + assert!(replay.await.unwrap().is_err()); + request(&mut current, 1, Operation::Bind).await; + assert_eq!(reply(&mut current).await.state, "ready"); + drop(current); + task.await.unwrap().unwrap(); + } + + #[tokio::test] + async fn private_dns_uses_controlled_resolver_and_joins_upstream_on_revocation() { + for return_answer in [true, false] { + let mut f = Fixture::new().await; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let mut cfg = ProxyConfig::new( + "127.0.0.1:0".parse().unwrap(), + f.registry.clone(), + Arc::new(CertMint::new(f.ca.clone())), + ); + cfg.dns_upstream = listener.local_addr().unwrap(); + f.proxy = Arc::new(Proxy::new(cfg)); + let query = b"\x00\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03api\x07example\x00\x00\x01\x00\x01"; + let (seen, received) = tokio::sync::oneshot::channel(); + let upstream = tokio::spawn(async move { + let (mut socket, _) = listener.accept().await.unwrap(); + let length = socket.read_u16().await.unwrap(); + let mut data = vec![0; length as usize]; + socket.read_exact(&mut data).await.unwrap(); + assert_eq!(data, query); + seen.send(()).unwrap(); + if return_answer { + data[2] |= 0x80; + socket.write_u16(length).await.unwrap(); + socket.write_all(&data).await.unwrap(); + } else { + let mut byte = [0]; + assert_eq!( + socket.read(&mut byte).await.unwrap(), + 0, + "revocation must close the controlled upstream" + ); + } + }); + let (mut c, task) = f.start(f.allocation.clone()); + reply(&mut c).await; + request(&mut c, 1, Operation::Bind).await; + reply(&mut c).await; + request( + &mut c, + 2, + Operation::Dns { + destination: f.allocation.gateway_ip, + port: 53, + }, + ) + .await; + let mut dns = handoff(&c).await; + reply(&mut c).await; + dns.write_u16(query.len() as u16).await.unwrap(); + dns.write_all(query).await.unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(1), received) + .await + .unwrap() + .unwrap(); + if return_answer { + let length = dns.read_u16().await.unwrap(); + let mut response = vec![0; length as usize]; + dns.read_exact(&mut response).await.unwrap(); + let mut expected = query.to_vec(); + expected[2] |= 0x80; + assert_eq!(response, expected); + } + let helper = tokio::spawn(async move { + assert!(wire::read::<_, Reply>(&mut c).await.is_err()); + drop(c); + }); + tokio::time::timeout( + std::time::Duration::from_secs(2), + f.registry.quarantine_and_drain(f.allocation.session_id), + ) + .await + .unwrap(); + helper.await.unwrap(); + upstream.await.unwrap(); + task.await.unwrap().unwrap(); + } + } + #[tokio::test] + async fn two_allocations_keep_dns_identity_and_revocation_is_local() { + let f = Fixture::new().await; + let mut other = (*f.registry.lookup_session(f.allocation.session_id).unwrap()).clone(); + other.session_id = SessionId::new(); + other.sandbox_id = SandboxId::new(); + other.guest_ip = "192.0.2.6".parse().unwrap(); + other.managed_deny = Some(ManagedDenyPolicy::new(1, ["api.example".into()]).unwrap()); + let mut a = f.allocation.clone(); + a.session_id = other.session_id; + a.sandbox_id = other.sandbox_id; + a.guest_ip = other.guest_ip; + a.gateway_ip = "192.0.2.5".parse().unwrap(); + a.boot_attempt = "boot-2".into(); + a.policy_digest = other.managed_deny.as_ref().unwrap().digest.clone(); + f.registry.apply(other).await.unwrap(); + let (mut first, one) = f.start(f.allocation.clone()); + reply(&mut first).await; + let (mut second, two) = f.start(a.clone()); + reply(&mut second).await; + request(&mut second, 1, Operation::Bind).await; + reply(&mut second).await; + let first_closed = tokio::spawn(async move { + assert!(wire::read::<_, Reply>(&mut first).await.is_err()); + drop(first); + }); + f.registry + .quarantine_and_drain(f.allocation.session_id) + .await; + first_closed.await.unwrap(); + one.await.unwrap().unwrap(); + request( + &mut second, + 2, + Operation::Dns { + destination: a.gateway_ip, + port: 53, + }, + ) + .await; + let mut dns = handoff(&second).await; + reply(&mut second).await; + let query=b"\x00\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03api\x07example\x00\x00\x01\x00\x01"; + dns.write_u16(query.len() as u16).await.unwrap(); + dns.write_all(query).await.unwrap(); + let size = dns.read_u16().await.unwrap(); + let mut answer = vec![0; size as usize]; + dns.read_exact(&mut answer).await.unwrap(); + assert_eq!( + answer[3] & 15, + 3, + "the second allocation denies api.example independently" + ); + request(&mut second, 3, Operation::Close).await; + assert_eq!(reply(&mut second).await.state, "closed"); + drop(second); + two.await.unwrap().unwrap(); + } +} diff --git a/crates/engram-host-agent/src/proxyd_client.rs b/crates/engram-host-agent/src/proxyd_client.rs index 71a1f8978..afeb5c530 100644 --- a/crates/engram-host-agent/src/proxyd_client.rs +++ b/crates/engram-host-agent/src/proxyd_client.rs @@ -176,6 +176,20 @@ impl ProxydHandle { } } + /// Dedicated allocation capability, initially CLOSED. This is not a final + /// policy receipt. A new boot or channel loss requires a fresh allocation. + pub async fn register_private_network( + &self, + allocation: &engram_egress_proto::private::Allocation, + ) -> Result { + let stream = tokio::net::UnixStream::connect(&self.control_sock) + .await + .map_err(|e| ProxydError(format!("private network connect: {e}")))?; + engram_egress_proto::private::register(stream, allocation) + .await + .map_err(|e| ProxydError(format!("private network register: {e}"))) + } + pub async fn hello(&self) -> Result { match self .request(&ToProxyd::Hello { diff --git a/docs/adr/0125-isolated-external-mcp.md b/docs/adr/0125-isolated-external-mcp.md index d26887b43..0d7a358e9 100644 --- a/docs/adr/0125-isolated-external-mcp.md +++ b/docs/adr/0125-isolated-external-mcp.md @@ -356,3 +356,38 @@ slice must install pre-execution host controls for all TCP ports and close unobserved UDP paths, including restore. Bootstrap receipts have no final binding hash and cannot authorize tool exposure. Local socket tests do not establish Firecracker, VZ, or harness compatibility acceptance. + +### Private network transport + +The selected VZ producer uses one bounded gVisor stack per VM. The helper owns +no host IP listener and cannot dial an external destination. The host passes +three owned descriptors: the Ethernet datagram channel, a reliable parent +control stream, and a registered private proxy stream. A private flow carries +its original IPv4 address and port and an SCM_RIGHTS stream. The Rust proxy +uses the existing session lease, TLS interceptor, and DNS policy. The private +stream is the allocation capability; no bearer credential is stored or logged. +Registration starts closed. An exact placement epoch must be bound before +packet admission. Replacement, policy change, or loss of either reliable +channel closes admission and joins owned work. A transport-ready response is +not a network proof or a final binding acknowledgement. + +The helper pins gvisor.dev/gvisor at +`v0.0.0-20260413194555-9680d69bf798` and Go 1.26.2. Ethernet MTU is 1500. +The host selects the exact source MAC, guest IPv4 address, and gateway. +Only IPv4 TCP and DNS at that gateway can reach the private proxy. Established +stack and DNS parsers own protocol decoding. There is no NAT fallback. +Firecracker will use an owned per-interface early fence, all-TCP redirection, +controlled DNS, and drops for other UDP; installed rule ordering still needs +Linux acceptance. Managed recovery will cold-boot from eligible disk-only +artifacts. Saved session RAM and device state are not eligible. Backend +activation, durable boot permits, packaging, and platform acceptance remain +separate work. This decision remains Proposed. + +A policy drain retains its session lease until the helper returns cleanup EOF +on the reliable private stream. The helper first closes packet admission and +joins its stack, packet, flow, DNS, and control work. The proxy drains pending +descriptor carriers while it waits, with a five-second limit. A failed barrier +poisons the stable session gate; policy replay or a fresh allocation cannot +reopen it in that proxy incarnation. Recovery needs a fresh proxy incarnation +and full policy replay with quarantine checks. Other sessions remain usable. +This barrier cannot revoke bytes that were delivered before it completed. diff --git a/sandbox-netstack/README.md b/sandbox-netstack/README.md new file mode 100644 index 000000000..b74d69ad8 --- /dev/null +++ b/sandbox-netstack/README.md @@ -0,0 +1,154 @@ +# Private sandbox network helper + +This artifact provides one IPv4 netstack per VM. It has no host IP listener, +route mutation, external resolver, destination dial, switch, or NAT. It does not +activate a backend or supply a final managed-policy proof. Creation remains +blocked until the backend and durable boot producer supply their own checks. + +Build with Go **1.26.2**, `GOTOOLCHAIN=local`. The exact module closure is in +`go.mod` and `go.sum`. The gVisor pin is +`v0.0.0-20260413194555-9680d69bf798`; its module hash is +`h1:7h21uRYtzPJjXp9W/BZLx68z8TwW8+h5BPdI4P5ZkJ0=` and its module-file hash is +`h1:xQ2PWgHmWJA/Ph4i1q1jBm39BKhc3W0DXqWoDSyuBOY=`. DNS decoding uses +`golang.org/x/net/dns/dnsmessage` v0.44.0. No floating dependency is allowed. + +From the repository root: + +```sh +sandbox-netstack/build.sh test +sandbox-netstack/build.sh race +sandbox-netstack/build.sh build +``` + +The optional second argument selects an owned output directory. The default is +`var/mcp-netstack-build`. The script rejects cache symlinks and uses that directory +for every Go cache and temporary file. The build writes +`engram-sandbox-netstack` there, with `CGO_ENABLED=0` and `-trimpath`. The race gate +uses `CGO_ENABLED=1` and needs a native C compiler. The tests build and execute the +real helper, so the Go tool must be available when tests run. Packaging and CI +must run these commands explicitly; Cargo does not run this module. + +## Descriptor and startup contract + +Start the executable with no arguments. Supply only these three descriptors; +use the process API's stdin/stdout/stderr descriptor mapping. These descriptors +are channels, **not logs**. Clear the child environment. All other descriptors +must be close-on-exec. Retain the parent control peer and child process handle. + +| Descriptor | Type | Owner and use | +|---|---|---| +| 0 | Connected AF_UNIX datagram | One VM Ethernet attachment; one datagram per frame | +| 1 | Connected AF_UNIX stream | Parent lifetime and explicit OPEN/CLOSE control | +| 2 | Connected AF_UNIX stream | Registered private proxy allocation capability | + +The helper duplicates each inherited descriptor into Go's poller and closes the +original. The parent must close its child-side copies immediately after spawn. +The parent must terminate and reap the helper on every failed startup, socket +loss, VM teardown, or invalidated generation. Do not infer lifetime from datagram +EOF. Do not pass the capability through argv, environment, disk, guest data, or +operational logs. Do not retain a second copy across host process generations. + +Register descriptor 2 first through `ProxydHandle::register_private_network`, or +`engram_egress_proto::private::register` on a dedicated host UDS connection. The +host-only upgrade is `ToProxyd::PrivateNetwork`, followed by the bounded +`private::Allocation`. It binds host ID/incarnation, proxy incarnation, +session/sandbox, boot attempt, attachment generation, exact guest/gateway IPv4, +and the current managed policy revision/digest. Registration requires a live +managed policy, starts CLOSED, and has no placement epoch. The returned socket +has consumed the CLOSED registration reply; transfer that socket to the helper. + +The host sends this configuration on descriptor 1 within five seconds: + +```json +{"version":1,"guest":"192.0.2.2","gateway":"192.0.2.1","mac":"02:00:00:00:00:02"} +``` + +Addresses must match the registered allocation and actual attachment. The host +must record and check that exact shape in its backend proof. The gateway MAC is +`02:00:00:00:00:01`. The guest MAC must be a different unicast MAC. MTU is exactly +1500. Only Ethernet IPv4 and gateway ARP are accepted. The helper returns: + +```json +{"version":1,"sequence":0,"epoch":0,"state":"closed"} +``` + +After obtaining the actual placement epoch, the parent sends: + +```json +{"version":1,"kind":"open","epoch":7} +``` + +The helper binds that epoch once on the proxy channel, drains closed-phase +packets, then opens packet admission. The response is sequence 1, the same epoch, +and state `ready`. This is **transport readiness only**. It cannot satisfy the +backend network proof, durable boot release, or final tool-exposure ACK. + +CLOSE uses `{"version":1,"kind":"close","epoch":7}`. A successful response has +sequence 2 and state `closed`. CLOSE is terminal. OPEN replay, wrong epoch, +malformed control, or either reliable channel's loss invalidates the allocation. +There is no auto-reopen or reconnect. Use a new allocation after loss. + +## Private flow contract + +Frames are a 4-byte big-endian length followed by strict JSON, at most 2048 bytes. +The first byte may wait while idle. A partial frame and each response write have +a five-second budget. Every request has version 1, a strictly increasing sequence +starting at 1, and the exact bound epoch. Operations are nested: + +```json +{"version":1,"sequence":1,"epoch":7,"operation":{"kind":"bind"}} +{"version":1,"sequence":2,"epoch":7,"operation":{"kind":"tcp","destination":"198.51.100.7","port":8443}} +``` + +`bind` returns `ready`. `tcp` and `dns` require one connected AF_UNIX stream via +SCM_RIGHTS on a single byte `0xED`, immediately after the header. The receiver +sets close-on-exec and rejects extra descriptors, wrong socket types, unknown +fields, missing ports, sequence replay, wrong epochs, and oversized input. +`accepted` on the allocation channel means the flow is owned by the session +lease; it is not an allow decision. The handed-off TCP stream contains guest TLS +bytes and goes through the existing Rust policy dispatcher on every port. + +DNS uses `kind: "dns"`, the exact allocation gateway, and port 53. Its stream +contains one two-byte-length-prefixed DNS request and one matching response. +The existing Rust DNS name policy and resolver own the decision. The helper +has no external DNS socket. TCP 53 on any other address goes to the ordinary TLS +dispatcher and plaintext DNS fails. UDP to any other port/address is dropped. + +## Bounds and cancellation + +Each helper has 64 active flows, at most 8 DNS exchanges, 16 pending TCP +handshakes, a 64-packet link queue, and 64 KiB TCP send/receive buffers. The pinned +gVisor IPv4 reassembly cache trims at 4 MiB to 3 MiB. DNS bodies are limited to +4096 bytes; UDP DNS must also fit the MTU and cannot be fragmented. TCP fragments +are reassembled by gVisor. Other UDP, ICMP input, IPv6, VLAN, spoofed MAC/IPv4/ARP, +short frames, oversized frames, and `MSG_TRUNC` are rejected. + +DNS exchanges have a ten-second budget. TCP flow lifetime is capped at five +minutes. Packet output waits at most 250 ms. Gateway response writes wait at +most one second. OPEN drains at most 512 queued datagrams; an input flood fails +activation. A closed packet is never stored for later replay. + +Allocation identity comes only from the inherited capability. A guest IP, MAC, +header, or payload cannot select another session. The proxy holds the existing +stable session lease before reading policy or TLS. Replacement and revocation +cancel and join owned Rust TCP/DNS/HTTP2 work. Allocation tombstones are bounded +at 4096 per proxy incarnation; exhaustion fails closed. Attachment generations +must increase within a host incarnation. Retired host incarnations cannot return. +An unrelated session has a different admission/drain lock. + +Revocation uses the reliable private stream, never datagram EOF. The proxy +first cancels and joins its TCP/DNS work, closes its write half, and waits up to +five seconds for helper EOF. On that stop signal the helper closes its gate, +closes the packet descriptor, and joins all packet readers/writers, stack work, +flow/DNS work, and control readers before it closes the private descriptor. +The stable session lease remains held through this barrier. Pending descriptor +carriers are drained with an OS-maximum ancillary buffer and closed. + +If cleanup EOF is absent, malformed, or fails, the proxy poisons that session's +stable gate. Apply, full replacement, and removal cannot acknowledge success; +a fresh allocation or ordinary policy replay cannot reopen it. Other sessions +remain usable. Recovery requires a fresh proxy incarnation plus the existing +full policy replay and quarantine checks. The host adapter must treat any +failed cleanup as a failed enforcement receipt and replace the proxy before +recovery. A helper OPEN or CLOSE response is never the final enforcement ACK. +Bytes already delivered before the cleanup barrier cannot be revoked. diff --git a/sandbox-netstack/build.sh b/sandbox-netstack/build.sh new file mode 100755 index 000000000..846067fef --- /dev/null +++ b/sandbox-netstack/build.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail +mode=${1:-test} +root=$(cd "$(dirname "$0")/.." && pwd -P) +out=${2:-"$root/var/mcp-netstack-build"} +case "$mode" in build|test|race) ;; *) echo 'usage: build.sh [build|test|race] [owned-output-directory]' >&2; exit 2;; esac +# Cache/output ownership is explicit. Never follow a cache symlink. +[[ ! -L "$out" ]] || exit 1 +if [[ ! -e "$out" ]]; then + mkdir -m 700 "$out" + printf '%s\n' 'sandbox-netstack build outputs and Go caches' > "$out/ownership.txt" +fi +[[ -f "$out/ownership.txt" && ! -L "$out/ownership.txt" ]] || exit 1 +out=$(cd "$out" && pwd -P) +for dir in go-path go-mod go-cache tmp; do + [[ ! -L "$out/$dir" ]] || exit 1 + mkdir -p "$out/$dir" +done +export GOTOOLCHAIN=local GOMAXPROCS=2 CGO_ENABLED=0 +export GOPATH="$out/go-path" GOMODCACHE="$out/go-mod" GOCACHE="$out/go-cache" TMPDIR="$out/tmp" +[[ $(go env GOVERSION) == go1.26.2 ]] || { echo 'Go 1.26.2 is required' >&2; exit 1; } +cd "$root/sandbox-netstack" +case "$mode" in + build) go build -mod=readonly -trimpath -o "$out/engram-sandbox-netstack" . ;; + test) go test -mod=readonly -timeout 30s ./...; go vet ./...; go mod verify ;; + race) CGO_ENABLED=1 go test -mod=readonly -race -timeout 45s ./... ;; +esac diff --git a/sandbox-netstack/frame.go b/sandbox-netstack/frame.go new file mode 100644 index 000000000..08710fbe4 --- /dev/null +++ b/sandbox-netstack/frame.go @@ -0,0 +1,92 @@ +package main + +import ( + "bytes" + "errors" + "gvisor.dev/gvisor/pkg/tcpip/header" + "net" + "net/netip" + "syscall" +) + +const mtu = 1500 + +var gatewayMAC = []byte{2, 0, 0, 0, 0, 1} + +type config struct { + Version uint32 `json:"version"` + Guest string `json:"guest"` + Gateway string `json:"gateway"` + MAC string `json:"mac"` +} +type network struct { + guest, gateway netip.Addr + mac [6]byte +} + +func parseNetwork(c config) (network, error) { + var n network + var err error + n.guest, err = netip.ParseAddr(c.Guest) + if err != nil { + return n, err + } + n.gateway, err = netip.ParseAddr(c.Gateway) + if err != nil { + return n, err + } + mac, err := net.ParseMAC(c.MAC) + if err != nil || len(mac) != 6 || mac[0]&1 != 0 || bytes.Equal(mac, gatewayMAC) || bytes.Equal(mac, make([]byte, 6)) || c.Version != 1 || !n.guest.Is4() || !n.gateway.Is4() || !n.guest.IsGlobalUnicast() || !n.gateway.IsGlobalUnicast() || n.guest == n.gateway { + return n, errors.New("invalid network shape") + } + copy(n.mac[:], mac) + return n, nil +} +func (n network) accepts(frame []byte, flags int) bool { + if flags&(syscall.MSG_TRUNC|syscall.MSG_CTRUNC) != 0 || len(frame) < header.EthernetMinimumSize || len(frame) > mtu+header.EthernetMinimumSize { + return false + } + eth := header.Ethernet(frame) + if !bytes.Equal([]byte(eth.SourceAddress()), n.mac[:]) { + return false + } + body := frame[header.EthernetMinimumSize:] + switch eth.Type() { + case header.IPv4ProtocolNumber: + if !bytes.Equal([]byte(eth.DestinationAddress()), gatewayMAC) || len(body) < header.IPv4MinimumSize { + return false + } + ip := header.IPv4(body) + if !ip.IsValid(len(body)) || ip.SourceAddress().As4() != n.guest.As4() || int(ip.TotalLength()) != len(body) { + return false + } + switch ip.Protocol() { + case 6: + return true // TCP reassembly belongs to gVisor. + case 17: + // DNS messages are bounded; fragmented UDP is not an accepted transport. + if ip.FragmentOffset() != 0 || ip.Flags()&header.IPv4FlagMoreFragments != 0 || ip.DestinationAddress().As4() != n.gateway.As4() || len(ip.Payload()) < header.UDPMinimumSize { + return false + } + u := header.UDP(ip.Payload()) + return u.DestinationPort() == 53 && int(u.Length()) == len(ip.Payload()) + default: + return false + } + case header.ARPProtocolNumber: + if len(body) < header.ARPSize { + return false + } + a := header.ARP(body) + guest, gateway := n.guest.As4(), n.gateway.As4() + if !a.IsValid() || !bytes.Equal(a.HardwareAddressSender(), n.mac[:]) || !bytes.Equal(a.ProtocolAddressSender(), guest[:]) || !bytes.Equal(a.ProtocolAddressTarget(), gateway[:]) { + return false + } + if a.Op() == header.ARPRequest { + return bytes.Equal([]byte(eth.DestinationAddress()), gatewayMAC) || eth.DestinationAddress() == header.EthernetBroadcastAddress + } + return a.Op() == header.ARPReply && bytes.Equal([]byte(eth.DestinationAddress()), gatewayMAC) && bytes.Equal(a.HardwareAddressTarget(), gatewayMAC) + default: + return false + } +} diff --git a/sandbox-netstack/frame_test.go b/sandbox-netstack/frame_test.go new file mode 100644 index 000000000..feea90253 --- /dev/null +++ b/sandbox-netstack/frame_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "net/netip" + "syscall" + "testing" +) + +func testNetwork() network { + return network{netip.MustParseAddr("192.0.2.2"), netip.MustParseAddr("192.0.2.1"), [6]byte{2, 0, 0, 0, 0, 2}} +} +func ipv4Frame(src [4]byte, protocol uint8, destination [4]byte, port uint16) []byte { + b := make([]byte, 14+20+20) + header.Ethernet(b).Encode(&header.EthernetFields{SrcAddr: tcpip.LinkAddress("\x02\x00\x00\x00\x00\x02"), DstAddr: tcpip.LinkAddress("\x02\x00\x00\x00\x00\x01"), Type: header.IPv4ProtocolNumber}) + header.IPv4(b[14:]).Encode(&header.IPv4Fields{TotalLength: 40, Protocol: protocol, SrcAddr: tcpip.AddrFrom4(src), DstAddr: tcpip.AddrFrom4(destination)}) + if protocol == 17 { + header.UDP(b[34:]).Encode(&header.UDPFields{SrcPort: 10000, DstPort: port, Length: 20}) + } + return b +} +func TestPacketAdmission(t *testing.T) { + n := testNetwork() + valid := ipv4Frame([4]byte{192, 0, 2, 2}, 6, [4]byte{198, 51, 100, 7}, 8443) + if !n.accepts(valid, 0) { + t.Fatal("valid all-port TCP rejected") + } + spoof := append([]byte(nil), valid...) + spoof[6] = 4 + ipv6 := append([]byte(nil), valid...) + ipv6[12] = 0x86 + ipv6[13] = 0xdd + vlan := append([]byte(nil), valid...) + vlan[12] = 0x81 + vlan[13] = 0 + for name, b := range map[string][]byte{ + "short": {1, 2, 3}, "oversized": make([]byte, 1515), "mac": spoof, "ipv6": ipv6, "vlan": vlan, + "ip": ipv4Frame([4]byte{192, 0, 2, 3}, 6, [4]byte{198, 51, 100, 7}, 443), + "other_udp": ipv4Frame([4]byte{192, 0, 2, 2}, 17, [4]byte{192, 0, 2, 1}, 9000), + "other_dns": ipv4Frame([4]byte{192, 0, 2, 2}, 17, [4]byte{198, 51, 100, 53}, 53), + "icmp": ipv4Frame([4]byte{192, 0, 2, 2}, 1, [4]byte{198, 51, 100, 7}, 0), + } { + t.Run(name, func(t *testing.T) { + if n.accepts(b, 0) { + t.Fatal("unsafe frame admitted") + } + }) + } + if n.accepts(valid, syscall.MSG_TRUNC) { + t.Fatal("truncated datagram admitted") + } + if !n.accepts(ipv4Frame([4]byte{192, 0, 2, 2}, 17, [4]byte{192, 0, 2, 1}, 53), 0) { + t.Fatal("controlled DNS rejected") + } +} + +func TestARPSourceAndGatewayAreExact(t *testing.T) { + n := testNetwork() + b := make([]byte, header.EthernetMinimumSize+header.ARPSize) + header.Ethernet(b).Encode(&header.EthernetFields{SrcAddr: tcpip.LinkAddress(n.mac[:]), DstAddr: header.EthernetBroadcastAddress, Type: header.ARPProtocolNumber}) + a := header.ARP(b[14:]) + a.SetIPv4OverEthernet() + a.SetOp(header.ARPRequest) + copy(a.HardwareAddressSender(), n.mac[:]) + copy(a.ProtocolAddressSender(), []byte{192, 0, 2, 2}) + copy(a.ProtocolAddressTarget(), []byte{192, 0, 2, 1}) + if !n.accepts(b, 0) { + t.Fatal("valid gateway ARP rejected") + } + for _, offset := range []int{14 + 8, 14 + 14, 14 + 24} { + forged := append([]byte(nil), b...) + forged[offset] ^= 1 + if n.accepts(forged, 0) { + t.Fatalf("forged ARP field at %d accepted", offset) + } + } +} diff --git a/sandbox-netstack/go.mod b/sandbox-netstack/go.mod new file mode 100644 index 000000000..09a2af056 --- /dev/null +++ b/sandbox-netstack/go.mod @@ -0,0 +1,15 @@ +module engrams/sandbox-netstack + +go 1.26.2 + +require ( + golang.org/x/net v0.44.0 + gvisor.dev/gvisor v0.0.0-20260413194555-9680d69bf798 +) + +require ( + github.com/google/btree v1.1.2 // indirect + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/time v0.12.0 // indirect +) diff --git a/sandbox-netstack/go.sum b/sandbox-netstack/go.sum new file mode 100644 index 000000000..27255c920 --- /dev/null +++ b/sandbox-netstack/go.sum @@ -0,0 +1,12 @@ +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +gvisor.dev/gvisor v0.0.0-20260413194555-9680d69bf798 h1:7h21uRYtzPJjXp9W/BZLx68z8TwW8+h5BPdI4P5ZkJ0= +gvisor.dev/gvisor v0.0.0-20260413194555-9680d69bf798/go.mod h1:xQ2PWgHmWJA/Ph4i1q1jBm39BKhc3W0DXqWoDSyuBOY= diff --git a/sandbox-netstack/helper_test.go b/sandbox-netstack/helper_test.go new file mode 100644 index 000000000..2365c3340 --- /dev/null +++ b/sandbox-netstack/helper_test.go @@ -0,0 +1,540 @@ +package main + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "golang.org/x/net/dns/dnsmessage" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/channel" + "gvisor.dev/gvisor/pkg/tcpip/link/ethernet" + "gvisor.dev/gvisor/pkg/tcpip/network/arp" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "io" + "net" + "os/exec" + "path/filepath" + "sync" + "syscall" + "testing" + "time" +) + +func datagramPair(t *testing.T) (*net.UnixConn, *net.UnixConn) { + t.Helper() + fd, e := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_DGRAM, 0) + if e != nil { + t.Fatal(e) + } + a, e := ownedUnix(fd[0]) + if e != nil { + t.Fatal(e) + } + b, e := ownedUnix(fd[1]) + if e != nil { + t.Fatal(e) + } + return a, b +} +func TestParentLossClosesClosedHelper(t *testing.T) { + packet, guest := datagramPair(t) + defer guest.Close() + parent, control, e := socketPair() + if e != nil { + t.Fatal(e) + } + defer parent.Close() + proxy, private, e := socketPair() + if e != nil { + t.Fatal(e) + } + defer proxy.Close() + done := make(chan error, 1) + go func() { done <- run(packet, control, private) }() + if e = writeFrame(parent, config{1, "192.0.2.2", "192.0.2.1", "02:00:00:00:00:02"}); e != nil { + t.Fatal(e) + } + parent.SetReadDeadline(time.Now().Add(2 * time.Second)) + var r reply + if e = readFrame(parent, &r); e != nil || r.State != "closed" { + t.Fatalf("closed readiness: %v %v", r, e) + } + parent.Close() + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("parent loss left helper alive") + } + proxy.SetReadDeadline(time.Now().Add(time.Second)) + var b [1]byte + if n, _ := proxy.Read(b[:]); n != 0 { + t.Fatal("private data after parent loss") + } +} + +// The fixture uses only inherited Unix streams. It is a transport peer, not a +// replacement for the Rust policy tests in engram-egress-proxyd. +func receiveFD(c *net.UnixConn) (*net.UnixConn, error) { + var carrier [1]byte + control := make([]byte, syscall.CmsgSpace(4)) + n, o, flags, _, e := c.ReadMsgUnix(carrier[:], control) + if e != nil { + return nil, e + } + if n != 1 || carrier[0] != 0xED || flags != 0 { + return nil, fmt.Errorf("bad descriptor carrier") + } + messages, e := syscall.ParseSocketControlMessage(control[:o]) + if e != nil { + return nil, e + } + fds, e := syscall.ParseUnixRights(&messages[0]) + if e != nil { + return nil, e + } + if len(fds) != 1 { + return nil, fmt.Errorf("bad descriptor count") + } + return ownedUnix(fds[0]) +} +func TestNetstackAllTCPAndControlledDNS(t *testing.T) { + packet, guest := datagramPair(t) + parent, control, e := socketPair() + if e != nil { + t.Fatal(e) + } + proxy, private, e := socketPair() + if e != nil { + t.Fatal(e) + } + done := make(chan error, 1) + go func() { done <- run(packet, control, private) }() + t.Cleanup(func() { + parent.Close() + proxy.Close() + guest.Close() + select { + case <-done: + case <-time.After(3 * time.Second): + t.Error("helper did not join") + } + }) + events := make(chan operation, 16) + fixtureDone := make(chan struct{}) + go func() { + defer close(fixtureDone) + defer proxy.Close() + var flows sync.WaitGroup + defer flows.Wait() + for { + var r request + if readFrame(proxy, &r) != nil { + return + } + state := "accepted" + if r.Operation.Kind == "bind" { + state = "ready" + } else if r.Operation.Kind == "close" { + state = "closed" + } else { + c, e := receiveFD(proxy) + if e != nil { + return + } + events <- r.Operation + flows.Go(func() { + defer c.Close() + c.SetDeadline(time.Now().Add(2 * time.Second)) + if r.Operation.Kind == "tcp" { + _, _ = io.Copy(c, c) + return + } + var length [2]byte + if _, e := io.ReadFull(c, length[:]); e != nil { + return + } + data := make([]byte, binary.BigEndian.Uint16(length[:])) + if _, e := io.ReadFull(c, data); e != nil { + return + } + var q dnsmessage.Message + if q.Unpack(data) != nil { + return + } + q.Response = true + q.RecursionAvailable = true + q.Answers = []dnsmessage.Resource{{Header: dnsmessage.ResourceHeader{Name: q.Questions[0].Name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 1}, Body: &dnsmessage.AResource{A: [4]byte{198, 51, 100, 7}}}} + out, e := q.Pack() + if e != nil { + return + } + binary.BigEndian.PutUint16(length[:], uint16(len(out))) + _, _ = c.Write(append(length[:], out...)) + }) + } + if writeFrame(proxy, reply{1, r.Sequence, r.Epoch, state}) != nil { + return + } + if state == "closed" { + return + } + } + }() + t.Cleanup(func() { + proxy.Close() + select { + case <-fixtureDone: + case <-time.After(3 * time.Second): + t.Error("fixture did not join") + } + }) + if e = writeFrame(parent, config{1, "192.0.2.2", "192.0.2.1", "02:00:00:00:00:02"}); e != nil { + t.Fatal(e) + } + var response reply + if e = readFrame(parent, &response); e != nil { + t.Fatal(e) + } + // Queue a valid TCP SYN while closed. The helper must discard it before OPEN. + closedSYN := ipv4Frame([4]byte{192, 0, 2, 2}, 6, [4]byte{198, 51, 100, 7}, 8443) + ip := header.IPv4(closedSYN[14:]) + ip.SetTTL(64) + ip.SetChecksum(^ip.CalculateChecksum()) + segment := header.TCP(closedSYN[34:]) + segment.Encode(&header.TCPFields{SrcPort: 12345, DstPort: 8443, SeqNum: 1, DataOffset: 20, Flags: header.TCPFlagSyn, WindowSize: 65535}) + segment.SetChecksum(^segment.CalculateChecksum(header.PseudoHeaderChecksum(tcp.ProtocolNumber, ip.SourceAddress(), ip.DestinationAddress(), 20))) + for range 8 { + guest.Write(closedSYN) + } + if e = writeFrame(parent, command{1, "open", 7}); e != nil { + t.Fatal(e) + } + if e = readFrame(parent, &response); e != nil || response.State != "ready" { + t.Fatalf("open: %v %v", response, e) + } + select { + case op := <-events: + t.Fatalf("closed traffic replayed: %v", op) + default: + } + guest.SetReadDeadline(time.Now().Add(40 * time.Millisecond)) + var unexpected [1514]byte + if size, e := guest.Read(unexpected[:]); e == nil { + t.Fatalf("closed SYN replay produced %d output bytes", size) + } + guest.SetReadDeadline(time.Time{}) + s, stopGuest := guestStack(t, guest) + defer stopGuest() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + for _, port := range []uint16{443, 8443, 80, 53} { + c, e := gonet.DialContextTCP(ctx, s, tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFrom4([4]byte{198, 51, 100, 7}), Port: port}, ipv4.ProtocolNumber) + if e != nil { + t.Fatalf("TCP %d: %v", port, e) + } + c.SetDeadline(time.Now().Add(2 * time.Second)) + sentinel := []byte("session=forged") + if _, e = c.Write(sentinel); e != nil { + t.Fatal(e) + } + got := make([]byte, len(sentinel)) + _, e = io.ReadFull(c, got) + c.Close() + if e != nil || !bytes.Equal(got, sentinel) { + t.Fatalf("TCP echo: %q %v", got, e) + } + select { + case op := <-events: + if op.Kind != "tcp" || op.Port != port || op.Destination != "198.51.100.7" { + t.Fatalf("tuple: %v", op) + } + case <-time.After(2 * time.Second): + t.Fatalf("no handoff for completed TCP echo on port %d", port) + } + } + q := dnsmessage.Message{Header: dnsmessage.Header{ID: 19, RecursionDesired: true}, Questions: []dnsmessage.Question{{Name: dnsmessage.MustNewName("fixture.example."), Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET}}} + query, _ := q.Pack() + for _, transport := range []string{"udp", "tcp"} { + address := tcpip.FullAddress{NIC: 1, Addr: tcpip.AddrFrom4([4]byte{192, 0, 2, 1}), Port: 53} + var c net.Conn + if transport == "udp" { + c, e = gonet.DialUDP(s, nil, &address, ipv4.ProtocolNumber) + } else { + c, e = gonet.DialContextTCP(ctx, s, address, ipv4.ProtocolNumber) + } + if e != nil { + t.Fatal(e) + } + c.SetDeadline(time.Now().Add(2 * time.Second)) + out := query + if transport == "tcp" { + out = append([]byte{byte(len(query) >> 8), byte(len(query))}, query...) + } + if _, e = c.Write(out); e != nil { + t.Fatal(e) + } + data := make([]byte, 4096) + var count int + if transport == "tcp" { + var size [2]byte + _, e = io.ReadFull(c, size[:]) + if e == nil { + count = int(binary.BigEndian.Uint16(size[:])) + _, e = io.ReadFull(c, data[:count]) + } + } else { + count, e = c.Read(data) + } + c.Close() + if e != nil { + t.Fatalf("DNS %s: %v", transport, e) + } + var answer dnsmessage.Message + if e = answer.Unpack(data[:count]); e != nil || len(answer.Answers) != 1 { + t.Fatalf("DNS %s: %v %v", transport, answer, e) + } + select { + case op := <-events: + if op.Kind != "dns" || op.Destination != "192.0.2.1" { + t.Fatalf("DNS tuple: %v", op) + } + case <-time.After(2 * time.Second): + t.Fatal("no DNS handoff") + } + } + if e = writeFrame(parent, command{1, "close", 7}); e != nil { + t.Fatal(e) + } + if e = readFrame(parent, &response); e != nil || response.State != "closed" { + t.Fatalf("close: %v %v", response, e) + } +} +func guestStack(t *testing.T, wire *net.UnixConn) (*stack.Stack, func()) { + t.Helper() + ep := channel.New(64, 1500, tcpip.LinkAddress("\x02\x00\x00\x00\x00\x02")) + s := stack.New(stack.Options{NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, arp.NewProtocol}, TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol}}) + if e := s.CreateNIC(1, ethernet.New(ep)); e != nil { + t.Fatal(e) + } + if e := s.AddProtocolAddress(1, tcpip.ProtocolAddress{Protocol: ipv4.ProtocolNumber, AddressWithPrefix: tcpip.AddressWithPrefix{Address: tcpip.AddrFrom4([4]byte{192, 0, 2, 2}), PrefixLen: 24}}, stack.AddressProperties{}); e != nil { + t.Fatal(e) + } + s.SetRouteTable([]tcpip.Route{{Destination: header.IPv4EmptySubnet, Gateway: tcpip.AddrFrom4([4]byte{192, 0, 2, 1}), NIC: 1}}) + ctx, cancel := context.WithCancel(context.Background()) + var work sync.WaitGroup + work.Go(func() { + for { + p := ep.ReadContext(ctx) + if p == nil { + return + } + v := p.ToView() + _, e := wire.Write(v.AsSlice()) + v.Release() + p.DecRef() + if e != nil { + return + } + } + }) + work.Go(func() { + b := make([]byte, 1515) + for { + n, e := wire.Read(b) + if e != nil { + return + } + p := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(append([]byte(nil), b[:n]...))}) + ep.InjectInbound(header.Ethernet(b[:n]).Type(), p) + p.DecRef() + } + }) + return s, func() { + cancel() + wire.Close() + ep.Close() + s.Close() + work.Wait() + s.Wait() + } +} + +func TestInheritedProcessLosesParentAndProxy(t *testing.T) { + binary := filepath.Join(t.TempDir(), "netstack") + build := exec.Command("go", "build", "-mod=readonly", "-trimpath", "-o", binary, ".") + if output, e := build.CombinedOutput(); e != nil { + t.Fatalf("build helper: %v %s", e, output) + } + for _, lost := range []string{"parent", "proxy"} { + t.Run(lost, func(t *testing.T) { + packet, guest := datagramPair(t) + defer guest.Close() + parent, control, e := socketPair() + if e != nil { + t.Fatal(e) + } + defer parent.Close() + proxy, private, e := socketPair() + if e != nil { + t.Fatal(e) + } + defer proxy.Close() + cmd := exec.Command(binary) + cmd.Env = []string{} + f0, e := packet.File() + if e != nil { + t.Fatal(e) + } + f1, e := control.File() + if e != nil { + t.Fatal(e) + } + f2, e := private.File() + if e != nil { + t.Fatal(e) + } + cmd.Stdin = f0 + cmd.Stdout = f1 + cmd.Stderr = f2 + e = cmd.Start() + f0.Close() + f1.Close() + f2.Close() + packet.Close() + control.Close() + private.Close() + if e != nil { + t.Fatal(e) + } + waited := make(chan error, 1) + go func() { waited <- cmd.Wait() }() + t.Cleanup(func() { + cmd.Process.Kill() + select { + case <-waited: + case <-time.After(2 * time.Second): + t.Error("child was not reaped") + } + }) + if e = writeFrame(parent, config{1, "192.0.2.2", "192.0.2.1", "02:00:00:00:00:02"}); e != nil { + t.Fatal(e) + } + parent.SetReadDeadline(time.Now().Add(2 * time.Second)) + var ready reply + if e = readFrame(parent, &ready); e != nil || ready.State != "closed" { + t.Fatalf("child ready: %v %v", ready, e) + } + if lost == "parent" { + parent.Close() + } else { + proxy.Close() + } + select { + case e = <-waited: + if e == nil { + t.Fatal("lost channel exit must be nonzero") + } + waited <- e + case <-time.After(2 * time.Second): + t.Fatal("lost channel left helper running") + } + }) + } +} + +func TestCloseJoinsHalfOpenTCP(t *testing.T) { + packet, guest := datagramPair(t) + defer guest.Close() + parent, control, e := socketPair() + if e != nil { + t.Fatal(e) + } + defer parent.Close() + proxy, private, e := socketPair() + if e != nil { + t.Fatal(e) + } + defer proxy.Close() + done := make(chan error, 1) + go func() { done <- run(packet, control, private) }() + t.Cleanup(func() { + parent.Close() + proxy.Close() + select { + case <-done: + case <-time.After(3 * time.Second): + t.Error("half-open helper did not join") + } + }) + fixture := make(chan struct{}) + go func() { + defer close(fixture) + defer proxy.Close() + for { + var r request + if readFrame(proxy, &r) != nil { + return + } + state := "ready" + if r.Operation.Kind == "close" { + state = "closed" + } + if r.Operation.Kind != "bind" && r.Operation.Kind != "close" { + return + } + if writeFrame(proxy, reply{1, r.Sequence, r.Epoch, state}) != nil || state == "closed" { + return + } + } + }() + t.Cleanup(func() { + proxy.Close() + select { + case <-fixture: + case <-time.After(time.Second): + t.Error("fixture not joined") + } + }) + if e = writeFrame(parent, config{1, "192.0.2.2", "192.0.2.1", "02:00:00:00:00:02"}); e != nil { + t.Fatal(e) + } + var response reply + if readFrame(parent, &response) != nil { + t.Fatal("no readiness") + } + if e = writeFrame(parent, command{1, "open", 7}); e != nil { + t.Fatal(e) + } + if readFrame(parent, &response) != nil { + t.Fatal("no open ack") + } + syn := ipv4Frame([4]byte{192, 0, 2, 2}, 6, [4]byte{198, 51, 100, 7}, 8443) + ip := header.IPv4(syn[14:]) + ip.SetTTL(64) + ip.SetChecksum(^ip.CalculateChecksum()) + segment := header.TCP(syn[34:]) + segment.Encode(&header.TCPFields{SrcPort: 12345, DstPort: 8443, SeqNum: 1, DataOffset: 20, Flags: header.TCPFlagSyn, WindowSize: 65535}) + segment.SetChecksum(^segment.CalculateChecksum(header.PseudoHeaderChecksum(tcp.ProtocolNumber, ip.SourceAddress(), ip.DestinationAddress(), 20))) + if _, e = guest.Write(syn); e != nil { + t.Fatal(e) + } + guest.SetReadDeadline(time.Now().Add(time.Second)) + var packetOut [1514]byte + if _, e = guest.Read(packetOut[:]); e != nil { + t.Fatalf("no handshake or neighbor request: %v", e) + } + if e = writeFrame(parent, command{1, "close", 7}); e != nil { + t.Fatal(e) + } + parent.SetReadDeadline(time.Now().Add(3 * time.Second)) + if e = readFrame(parent, &response); e != nil || response.State != "closed" { + t.Fatalf("close did not join half-open TCP: %v %v", response, e) + } +} diff --git a/sandbox-netstack/main.go b/sandbox-netstack/main.go new file mode 100644 index 000000000..8178d344e --- /dev/null +++ b/sandbox-netstack/main.go @@ -0,0 +1,501 @@ +// The only inherited descriptors are 0=packet, 1=parent control, 2=proxy +// capability. None is a log. No arguments, environment config, or listener. +package main + +import ( + "context" + "encoding/binary" + "errors" + "golang.org/x/net/dns/dnsmessage" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/channel" + "gvisor.dev/gvisor/pkg/tcpip/link/ethernet" + "gvisor.dev/gvisor/pkg/tcpip/network/arp" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" + "io" + "net" + "os" + "sync" + "syscall" + "time" +) + +const maxFlows = 64 +const maxDNS = 8 + +type gate struct { + mu sync.Mutex + open, dead bool + active, dns int + conns map[net.Conn]bool + work sync.WaitGroup +} + +func (g *gate) enter(dns bool) bool { + g.mu.Lock() + defer g.mu.Unlock() + if !g.open || g.dead || g.active >= maxFlows || (dns && g.dns >= maxDNS) { + return false + } + g.active++ + if dns { + g.dns++ + } + g.work.Add(1) + return true +} +func (g *gate) leave(dns bool) { + g.mu.Lock() + g.active-- + if dns { + g.dns-- + } + g.mu.Unlock() + g.work.Done() +} +func (g *gate) track(c net.Conn) bool { + g.mu.Lock() + defer g.mu.Unlock() + if !g.open || g.dead { + c.Close() + return false + } + g.conns[c] = true + return true +} +func (g *gate) untrack(c net.Conn) { c.Close(); g.mu.Lock(); delete(g.conns, c); g.mu.Unlock() } +func (g *gate) close() { + g.mu.Lock() + defer g.mu.Unlock() + g.open = false + g.dead = true + for c := range g.conns { + c.Close() + } +} +func main() { + if len(os.Args) != 1 { + os.Exit(1) + } + packet, e := ownedUnix(0) + if e != nil { + os.Exit(1) + } + control, e := ownedUnix(1) + if e != nil { + packet.Close() + os.Exit(1) + } + private, e := ownedUnix(2) + if e != nil { + packet.Close() + control.Close() + os.Exit(1) + } + if run(packet, control, private) != nil { + os.Exit(1) + } +} +func run(packet, control, private *net.UnixConn) error { + defer packet.Close() + defer control.Close() + defer private.Close() + var cfg config + control.SetReadDeadline(time.Now().Add(handoffTimeout)) + if err := readFrame(control, &cfg); err != nil { + return err + } + n, err := parseNetwork(cfg) + if err != nil { + return err + } + // Socket types are part of the attachment contract. + for conn, want := range map[*net.UnixConn]int{packet: syscall.SOCK_DGRAM, control: syscall.SOCK_STREAM, private: syscall.SOCK_STREAM} { + raw, e := conn.SyscallConn() + if e != nil { + return e + } + var actual int + var inner error + if e = raw.Control(func(fd uintptr) { actual, inner = syscall.GetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_TYPE) }); e != nil { + return e + } + if inner != nil || actual != want { + return errors.New("wrong private descriptor type") + } + } + g := &gate{conns: make(map[net.Conn]bool)} + stop := make(chan struct{}) + var once sync.Once + fail := func() { + once.Do(func() { + g.close() + close(stop) + packet.Close() + control.Close() + // Interrupt a pending handoff write without sending cleanup EOF. + private.SetWriteDeadline(time.Now()) + }) + } + defer fail() + i := &ingress{conn: private, replies: make(chan reply), stop: stop, fail: fail} + var channels sync.WaitGroup + defer func() { + fail() + // This defer runs after stack, packet, and flow cleanup below. EOF on + // this reliable stream is the proxy's cleanup acknowledgement. + private.SetReadDeadline(time.Now()) + channels.Wait() + private.Close() + }() + channels.Go(i.read) + commands := make(chan command) + channels.Go(func() { + for { + var c command + if readFrame(control, &c) != nil { + fail() + return + } + select { + case commands <- c: + case <-stop: + return + } + } + }) + ep := channel.New(64, mtu, tcpip.LinkAddress(gatewayMAC)) + s := stack.New(stack.Options{NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, arp.NewProtocol}, TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol}}) + defer func() { s.Close(); s.Wait() }() + if e := s.CreateNIC(1, ethernet.New(ep)); e != nil { + return errors.New(e.String()) + } + address := tcpip.AddrFrom4(n.gateway.As4()) + if e := s.AddProtocolAddress(1, tcpip.ProtocolAddress{Protocol: ipv4.ProtocolNumber, AddressWithPrefix: address.WithPrefix()}, stack.AddressProperties{}); e != nil { + return errors.New(e.String()) + } + if e := s.SetPromiscuousMode(1, true); e != nil { + return errors.New(e.String()) + } + if e := s.SetSpoofing(1, true); e != nil { + return errors.New(e.String()) + } + s.SetRouteTable([]tcpip.Route{{Destination: header.IPv4EmptySubnet, NIC: 1}}) + // Conservative per-flow buffers and bounded SYN retries. There is no load + // tuning knob at this boundary. + for _, option := range []tcpip.SettableTransportProtocolOption{ + &tcpip.TCPSendBufferSizeRangeOption{Min: 4096, Default: 65536, Max: 65536}, + &tcpip.TCPReceiveBufferSizeRangeOption{Min: 4096, Default: 65536, Max: 65536}, + } { + if e := s.SetTransportProtocolOption(tcp.ProtocolNumber, option); e != nil { + return errors.New(e.String()) + } + } + retries := tcpip.TCPSynRetriesOption(1) + if e := s.SetTransportProtocolOption(tcp.ProtocolNumber, &retries); e != nil { + return errors.New(e.String()) + } + exchangeDNS := func(data []byte) ([]byte, error) { + var m dnsmessage.Message + if len(data) == 0 || len(data) > dnsLimit { + return nil, errors.New("DNS size") + } + if e := m.Unpack(data); e != nil { + return nil, e + } + if m.Response || len(m.Questions) != 1 { + return nil, errors.New("DNS query shape") + } + c, e := i.flow("dns", n.gateway.String(), 53) + if e != nil { + return nil, e + } + if !g.track(c) { + return nil, errors.New("closed") + } + defer g.untrack(c) + c.SetDeadline(time.Now().Add(10 * time.Second)) + var size [2]byte + binary.BigEndian.PutUint16(size[:], uint16(len(data))) + if _, e = c.Write(append(size[:], data...)); e != nil { + return nil, e + } + if _, e = io.ReadFull(c, size[:]); e != nil { + return nil, e + } + length := int(binary.BigEndian.Uint16(size[:])) + if length == 0 || length > dnsLimit { + return nil, errors.New("DNS reply size") + } + out := make([]byte, length) + if _, e = io.ReadFull(c, out); e != nil { + return nil, e + } + var response dnsmessage.Message + if e = response.Unpack(out); e != nil { + return nil, e + } + if !response.Response || response.ID != m.ID || len(response.Questions) != 1 || response.Questions[0] != m.Questions[0] { + return nil, errors.New("DNS reply mismatch") + } + return out, nil + } + forward := tcp.NewForwarder(s, 65536, 16, func(r *tcp.ForwarderRequest) { + id := r.ID() + dns := id.LocalAddress == address && id.LocalPort == 53 + if id.RemoteAddress != tcpip.AddrFrom4(n.guest.As4()) || !g.enter(dns) { + r.Complete(true) + return + } + defer g.leave(dns) + var queue waiter.Queue + endpoint, tcpErr := r.CreateEndpoint(&queue) + r.Complete(false) + if tcpErr != nil { + return + } + c := gonet.NewTCPConn(&queue, endpoint) + if !g.track(c) { + return + } + defer g.untrack(c) + c.SetDeadline(time.Now().Add(5 * time.Minute)) + if dns { + c.SetDeadline(time.Now().Add(10 * time.Second)) + var size [2]byte + if _, e := io.ReadFull(c, size[:]); e != nil { + return + } + length := int(binary.BigEndian.Uint16(size[:])) + if length == 0 || length > dnsLimit { + return + } + data := make([]byte, length) + if _, e := io.ReadFull(c, data); e != nil { + return + } + out, e := exchangeDNS(data) + if e != nil { + return + } + binary.BigEndian.PutUint16(size[:], uint16(len(out))) + // Close sets the gate under this same lock; no response can start after it. + g.mu.Lock() + if g.open && !g.dead { + c.SetWriteDeadline(time.Now().Add(time.Second)) + _, _ = c.Write(append(size[:], out...)) + } + g.mu.Unlock() + return + } + upstream, e := i.flow("tcp", id.LocalAddress.String(), id.LocalPort) + if e != nil { + return + } + if !g.track(upstream) { + return + } + defer g.untrack(upstream) + upstream.SetDeadline(time.Now().Add(5 * time.Minute)) + finished := make(chan struct{}) + go func() { _, _ = io.Copy(upstream, c); upstream.CloseWrite(); close(finished) }() + _, _ = io.Copy(c, upstream) + c.Close() + upstream.Close() + <-finished + }) + s.SetTransportProtocolHandler(tcp.ProtocolNumber, forward.HandlePacket) + udpConn, e := gonet.DialUDP(s, &tcpip.FullAddress{NIC: 1, Addr: address, Port: 53}, nil, ipv4.ProtocolNumber) + if e != nil { + return e + } + var workers sync.WaitGroup + workers.Go(func() { + b := make([]byte, dnsLimit+1) + for { + length, peer, e := udpConn.ReadFrom(b) + if e != nil { + return + } + if length > dnsLimit || !g.enter(true) { + continue + } + data := append([]byte(nil), b[:length]...) + go func() { + defer g.leave(true) + out, e := exchangeDNS(data) + if e != nil { + return + } + g.mu.Lock() + defer g.mu.Unlock() + if g.open && !g.dead { + udpConn.SetWriteDeadline(time.Now().Add(time.Second)) + _, _ = udpConn.WriteTo(out, peer) + } + }() + } + }) + workers.Go(func() { + for { + p := ep.ReadContext(context.Background()) + if p == nil { + return + } + v := p.ToView() + var writeErr error + g.mu.Lock() + if g.open && !g.dead { + packet.SetWriteDeadline(time.Now().Add(250 * time.Millisecond)) + _, writeErr = packet.Write(v.AsSlice()) + } + g.mu.Unlock() + v.Release() + p.DecRef() + if writeErr != nil { + fail() + return + } + } + }) + activate := make(chan chan error) + workers.Go(func() { + b := make([]byte, mtu+15) + for { + select { + case result := <-activate: + // The receiver owns this drain. No pre-OPEN datagram remains queued, and + // an input flood fails activation instead of extending an unbounded drain. + raw, e := packet.SyscallConn() + if e == nil { + controlErr := raw.Control(func(fd uintptr) { + for count := 0; count < 512; count++ { + _, _, _, _, x := syscall.Recvmsg(int(fd), b, nil, syscall.MSG_DONTWAIT) + if x == syscall.EAGAIN || x == syscall.EWOULDBLOCK { + return + } + if x != nil { + e = x + return + } + } + e = errors.New("closed packet queue did not drain") + }) + if controlErr != nil { + e = controlErr + } + } + g.mu.Lock() + if e == nil && !g.dead { + g.open = true + } else if e == nil { + e = errors.New("closed") + } + g.mu.Unlock() + result <- e + default: + } + packet.SetReadDeadline(time.Now().Add(25 * time.Millisecond)) + length, _, flags, _, e := packet.ReadMsgUnix(b, nil) + if e != nil { + if timeout, ok := e.(net.Error); ok && timeout.Timeout() { + continue + } + g.mu.Lock() + dead := g.dead + g.mu.Unlock() + if !dead { + fail() + } + return + } + g.mu.Lock() + open := g.open && !g.dead + g.mu.Unlock() + if !open || !n.accepts(b[:length], flags) { + continue + } + p := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(append([]byte(nil), b[:length]...))}) + ep.InjectInbound(header.Ethernet(b[:length]).Type(), p) + p.DecRef() + } + }) + var cleanOnce sync.Once + cleanup := func() { + cleanOnce.Do(func() { + g.close() + packet.Close() + udpConn.Close() + ep.Close() + s.Close() + workers.Wait() + g.work.Wait() + s.Close() + s.Wait() + }) + } + defer func() { cleanup(); fail() }() + if e := writeFrame(control, reply{1, 0, 0, "closed"}); e != nil { + return e + } + opened := false + for { + select { + case <-stop: + return errors.New("private channel closed") + case c := <-commands: + if c.Version != 1 || c.Epoch == 0 || c.Epoch > 9007199254740991 { + return errors.New("invalid command") + } + switch c.Kind { + case "open": + if opened { + return errors.New("allocation already used") + } + opened = true + i.epoch = c.Epoch + if e := i.call(operation{Kind: "bind"}, nil); e != nil { + return e + } + result := make(chan error, 1) + select { + case activate <- result: + case <-stop: + return errors.New("closed") + } + select { + case e := <-result: + if e != nil { + return e + } + case <-stop: + return errors.New("closed") + } + if e := writeFrame(control, reply{1, 1, c.Epoch, "ready"}); e != nil { + return e + } + case "close": + if !opened || c.Epoch != i.epoch { + return errors.New("wrong epoch") + } + cleanup() + if e := i.call(operation{Kind: "close"}, nil); e != nil { + return e + } + if e := writeFrame(control, reply{1, 2, c.Epoch, "closed"}); e != nil { + return e + } + return nil + default: + return errors.New("unknown command") + } + } + } +} diff --git a/sandbox-netstack/protocol.go b/sandbox-netstack/protocol.go new file mode 100644 index 000000000..ffdeb9194 --- /dev/null +++ b/sandbox-netstack/protocol.go @@ -0,0 +1,225 @@ +package main + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "errors" + "io" + "net" + "os" + "sync" + "syscall" + "time" +) + +const headerLimit = 2048 +const dnsLimit = 4096 +const handoffTimeout = 5 * time.Second + +type operation struct { + Kind string `json:"kind"` + Destination string `json:"destination,omitempty"` + Port uint16 `json:"port,omitempty"` +} +type request struct { + Version uint32 `json:"version"` + Sequence uint64 `json:"sequence"` + Epoch uint64 `json:"epoch"` + Operation operation `json:"operation"` +} +type reply struct { + Version uint32 `json:"version"` + Sequence uint64 `json:"sequence"` + Epoch uint64 `json:"epoch"` + State string `json:"state"` +} +type command struct { + Version uint32 `json:"version"` + Kind string `json:"kind"` + Epoch uint64 `json:"epoch"` +} + +func readFrame(c *net.UnixConn, v any) error { + var head [4]byte + // Idle reliable channels stay live; partial frames have a fixed budget. + if _, err := io.ReadFull(c, head[:1]); err != nil { + return err + } + if err := c.SetReadDeadline(time.Now().Add(handoffTimeout)); err != nil { + return err + } + defer c.SetReadDeadline(time.Time{}) + if _, err := io.ReadFull(c, head[1:]); err != nil { + return err + } + length := binary.BigEndian.Uint32(head[:]) + if length == 0 || length > headerLimit { + return errors.New("invalid private header") + } + data := make([]byte, length) + if _, err := io.ReadFull(c, data); err != nil { + return err + } + d := json.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + if err := d.Decode(v); err != nil { + return err + } + if d.Decode(new(any)) != io.EOF { + return errors.New("extra private data") + } + return nil +} +func writeFrame(c *net.UnixConn, v any) error { + body, err := json.Marshal(v) + if err != nil { + return err + } + if len(body) > headerLimit { + return errors.New("private header too large") + } + data := make([]byte, 4, len(body)+4) + binary.BigEndian.PutUint32(data, uint32(len(body))) + data = append(data, body...) + if err = c.SetWriteDeadline(time.Now().Add(handoffTimeout)); err != nil { + return err + } + defer c.SetWriteDeadline(time.Time{}) + for len(data) > 0 { + n, e := c.Write(data) + if e != nil { + return e + } + data = data[n:] + } + return nil +} +func socketPair() (*net.UnixConn, *net.UnixConn, error) { + fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + if err != nil { + return nil, nil, err + } + syscall.CloseOnExec(fds[0]) + syscall.CloseOnExec(fds[1]) + a, e := ownedUnix(fds[0]) + if e != nil { + syscall.Close(fds[1]) + return nil, nil, e + } + b, e := ownedUnix(fds[1]) + if e != nil { + a.Close() + return nil, nil, e + } + return a, b, nil +} +func ownedUnix(fd int) (*net.UnixConn, error) { + f := os.NewFile(uintptr(fd), "private-network") + defer f.Close() + c, err := net.FileConn(f) + if err != nil { + return nil, err + } + unix, ok := c.(*net.UnixConn) + if !ok { + c.Close() + return nil, errors.New("expected Unix descriptor") + } + return unix, nil +} +func sendFD(c *net.UnixConn, fd *net.UnixConn) error { + raw, err := fd.SyscallConn() + if err != nil { + return err + } + var writeErr error + err = raw.Control(func(value uintptr) { + _, _, writeErr = c.WriteMsgUnix([]byte{0xED}, syscall.UnixRights(int(value)), nil) + }) + if err != nil { + return err + } + return writeErr +} + +type ingress struct { + conn *net.UnixConn + mu sync.Mutex + sequence, epoch uint64 + replies chan reply + stop <-chan struct{} + fail func() +} + +func (i *ingress) read() { + for { + var r reply + if readFrame(i.conn, &r) != nil { + i.fail() + return + } + select { + case i.replies <- r: + case <-i.stop: + return + } + if r.State == "closed" && r.Sequence > 0 { + return + } + } +} +func (i *ingress) call(op operation, fd *net.UnixConn) error { + i.mu.Lock() + defer i.mu.Unlock() + select { + case <-i.stop: + return errors.New("closed") + default: + } + i.sequence++ + if err := writeFrame(i.conn, request{1, i.sequence, i.epoch, op}); err != nil { + i.fail() + return err + } + if fd != nil { + i.conn.SetWriteDeadline(time.Now().Add(handoffTimeout)) + defer i.conn.SetWriteDeadline(time.Time{}) + if err := sendFD(i.conn, fd); err != nil { + i.fail() + return err + } + } + timer := time.NewTimer(handoffTimeout) + defer timer.Stop() + select { + case r := <-i.replies: + want := "accepted" + if op.Kind == "bind" { + want = "ready" + } + if op.Kind == "close" { + want = "closed" + } + if r.Version == 1 && r.Sequence == i.sequence && r.Epoch == i.epoch && r.State == want { + return nil + } + case <-timer.C: + case <-i.stop: + } + i.fail() + return errors.New("private handoff failed") +} +func (i *ingress) flow(kind, destination string, port uint16) (*net.UnixConn, error) { + a, b, err := socketPair() + if err != nil { + i.fail() + return nil, err + } + defer b.Close() + if err = i.call(operation{kind, destination, port}, b); err != nil { + a.Close() + return nil, err + } + return a, nil +} From 8447dc24eeff844eee3f2159bc7f2c6204394fde Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 11:41:23 -0500 Subject: [PATCH 033/279] Preserve external MCP results in native harnesses Tag immutable MCP manifest entries and validate the existing result envelope before native presentation. Preserve structured output, upstream errors, delivery status, and plan boundaries through deferred resume. Keep Claude MCP configuration strict for empty manifests. Append advisory call cancellation with original identity, retain pending context across disconnects, and add native adapter, wire, and cold-reopen coverage. Runtime cancellation routing and cold completion without saved identity remain composition requirements. --- crates/engram-coordinator/src/state.rs | 37 ++ crates/engram-harness-claude/src/main.rs | 611 +++++++++++++++--- crates/engram-harness-codex/src/main.rs | 404 +++++++++++- crates/engram-harness-proto/src/lib.rs | 14 +- .../engram-harness-proto/src/tool_result.rs | 267 ++++++++ .../tests/decode_never_panics.rs | 3 + .../event_tool_call_cancel_requested.bin | Bin 0 -> 55 bytes .../engram-harness-proto/tests/support/mod.rs | 8 + .../engram-harness-proto/tests/wire_golden.rs | 21 + crates/engram-harness-sdk/src/first_output.rs | 3 +- crates/engram-protocol/src/wire.rs | 3 +- crates/engram-protocol/tests/wire_golden.rs | 2 +- .../__tests__/mcp-session-bindings.test.ts | 2 + orchestrator/src/mcp/session-bindings.ts | 1 + orchestrator/src/tools/manifest.ts | 1 + 15 files changed, 1274 insertions(+), 103 deletions(-) create mode 100644 crates/engram-harness-proto/src/tool_result.rs create mode 100644 crates/engram-harness-proto/tests/golden/event_tool_call_cancel_requested.bin diff --git a/crates/engram-coordinator/src/state.rs b/crates/engram-coordinator/src/state.rs index e5d00fbea..6c122867e 100644 --- a/crates/engram-coordinator/src/state.rs +++ b/crates/engram-coordinator/src/state.rs @@ -199,6 +199,12 @@ pub enum SessionEvent { args_json: String, at: DateTime, }, + HarnessToolCallCancelRequested { + run_id: String, + tool_call_id: String, + name: String, + at: DateTime, + }, HarnessRunCompleted { run_id: String, ok: bool, @@ -541,6 +547,7 @@ impl SessionEvent { Self::HarnessToolCallCompleted { .. } => "tool_call_completed", Self::HarnessBrowserActivity { .. } => "browser_activity", Self::HarnessToolCallRequested { .. } => "tool_call_requested", + Self::HarnessToolCallCancelRequested { .. } => "tool_call_cancel_requested", Self::HarnessRunCompleted { .. } => "run_completed", Self::HarnessRunInterrupted { .. } => "run_interrupted", Self::HarnessRunContinued { .. } => "run_continued", @@ -644,6 +651,16 @@ impl SessionEvent { args_json, at, }, + HarnessEvent::ToolCallCancelRequested { + run_id, + call_id, + name, + } => Self::HarnessToolCallCancelRequested { + run_id, + tool_call_id: call_id, + name, + at, + }, HarnessEvent::RunCompleted { run_id, ok } => { Self::HarnessRunCompleted { run_id, ok, at } } @@ -1775,6 +1792,26 @@ fn strip_jsonb_nul(v: &mut serde_json::Value) -> bool { pub(crate) mod tests { use super::*; + #[test] + fn tool_cancel_requested_keeps_original_identity_without_completion() { + let event = SessionEvent::from_harness( + HarnessEvent::ToolCallCancelRequested { + run_id: "r1".into(), + call_id: "c1".into(), + name: "remote_inventory".into(), + }, + DateTime::from_timestamp(0, 0).unwrap(), + ); + assert_eq!(event.kind(), "tool_call_cancel_requested"); + assert_eq!(outbox_ack_id(SessionId::new(), &event), None); + let json = serde_json::to_value(event).unwrap(); + assert_eq!(json["run_id"], "r1"); + assert_eq!(json["tool_call_id"], "c1"); + assert_eq!(json["name"], "remote_inventory"); + assert!(json.get("ok").is_none()); + assert!(json.get("result_json").is_none()); + } + /// Incident 2026-07-10: PG jsonb rejects U+0000, so a /// `tool_call_completed` whose `result_summary` carried raw binary /// (an `xxd` of a corrupted file) failed the insert and silently diff --git a/crates/engram-harness-claude/src/main.rs b/crates/engram-harness-claude/src/main.rs index 8e02780de..c8f6a9fd0 100644 --- a/crates/engram-harness-claude/src/main.rs +++ b/crates/engram-harness-claude/src/main.rs @@ -30,13 +30,13 @@ // Cross-platform stub — vsock dialing is Linux-only, and the // adapter only ships inside FC rootfs / Linux ProcessBackend. -#[cfg(not(target_os = "linux"))] +#[cfg(all(not(target_os = "linux"), not(test)))] fn main() { eprintln!("engram-harness-claude is Linux-only"); std::process::exit(1); } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", test))] mod adapter { use std::collections::{HashMap, HashSet, VecDeque}; use std::os::unix::process::ExitStatusExt; @@ -47,6 +47,7 @@ mod adapter { use clap::Parser; use engram_core::SessionId; + use engram_harness_proto::tool_result::{McpToolResult, ResultFormat}; use engram_harness_proto::{ AgentRole, EditHunk, FileChange, HarnessCommand, HarnessEvent, MAX_FILE_CHANGE_BYTES, }; @@ -90,6 +91,8 @@ mod adapter { pub input_schema: Value, pub execution: ToolExecution, #[serde(default)] + pub result_format: ResultFormat, + #[serde(default)] pub native_bindings: NativeBindings, } @@ -134,6 +137,7 @@ mod adapter { description: browser_view::TOOL_DESCRIPTION.into(), input_schema: browser_view::input_schema(), execution: ToolExecution::Sync, + result_format: ResultFormat::Json, native_bindings: NativeBindings::default(), }); } @@ -1274,7 +1278,8 @@ mod adapter { /// request/response. pub mod mcp_bridge { use super::{ - browser_view, injected_tools, manifest_from_env, BufReader, StateDir, ToolManifest, + browser_view, injected_tools, manifest_from_env, BufReader, McpToolResult, + ResultFormat, StateDir, ToolManifest, }; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -1301,27 +1306,86 @@ mod adapter { .filter(|value| !value.is_empty()) .map(PathBuf::from) .unwrap_or_else(|| StateDir::from_env().mcp_sock()); - let mut lines = BufReader::new(tokio::io::stdin()).lines(); - let mut stdout = tokio::io::stdout(); + serve_io( + BufReader::new(tokio::io::stdin()), + tokio::io::stdout(), + &manifest, + &socket_path, + ) + .await + } + + pub async fn serve_io( + reader: R, + mut stdout: W, + manifest: &ToolManifest, + socket_path: &Path, + ) -> std::process::ExitCode { + let mut lines = reader.lines(); + let mut calls = tokio::task::JoinSet::new(); + let mut pending: std::collections::HashMap = + std::collections::HashMap::new(); loop { - let line = match lines.next_line().await { - Ok(Some(line)) => line, - Ok(None) => return std::process::ExitCode::SUCCESS, - Err(_) => return std::process::ExitCode::from(1), - }; - let request: Value = match serde_json::from_str(&line) { - Ok(request) => request, - Err(_) => { - let response = json_rpc_error(Value::Null, -32700, "Parse error"); - if write_response(&mut stdout, &response).await.is_err() { - return std::process::ExitCode::from(1); + tokio::select! { + completed = calls.join_next_with_id(), if !calls.is_empty() => { + match completed { + Some(Ok((task_id, (key, response)))) => { + // Cancellation and request-id reuse can race a completed task. + if pending.get(&key).is_some_and(|handle| handle.id() == task_id) { + pending.remove(&key); + if let Some(response) = response { + if write_response(&mut stdout, &response).await.is_err() { + return std::process::ExitCode::FAILURE; + } + } + } + } + Some(Err(error)) => pending.retain(|_, handle| handle.id() != error.id()), + None => {} } - continue; } - }; - if let Some(response) = handle_request(request, &manifest, &socket_path).await { - if write_response(&mut stdout, &response).await.is_err() { - return std::process::ExitCode::from(1); + line = lines.next_line() => { + let line = match line { + Ok(Some(line)) => line, + Ok(None) => { + calls.shutdown().await; + return std::process::ExitCode::SUCCESS; + } + Err(_) => return std::process::ExitCode::FAILURE, + }; + let request: Value = match serde_json::from_str(&line) { + Ok(request) => request, + Err(_) => { + if write_response(&mut stdout, &json_rpc_error(Value::Null, -32700, "Parse error")).await.is_err() { + return std::process::ExitCode::FAILURE; + } + continue; + } + }; + if request.get("id").is_none() { + if request.get("method").and_then(Value::as_str) == Some("notifications/cancelled") { + if let Some(id) = request.pointer("/params/requestId") { + if let Some(handle) = pending.remove(&id.to_string()) { handle.abort(); } + } + } + continue; + } + let id = request["id"].clone(); + let key = id.to_string(); + if pending.contains_key(&key) || pending.len() >= 128 { + if write_response(&mut stdout, &json_rpc_error(id, -32600, "Duplicate request or too many pending calls.")).await.is_err() { + return std::process::ExitCode::FAILURE; + } + continue; + } + let manifest = manifest.clone(); + let socket_path = socket_path.to_path_buf(); + let request_key = key.clone(); + let handle = calls.spawn(async move { + let response = handle_request(request, &manifest, &socket_path).await; + (request_key, response) + }); + pending.insert(key, handle); } } } @@ -1424,6 +1488,12 @@ mod adapter { .pointer("/params/_meta/claudecode~1toolUseId") .and_then(Value::as_str); match round_trip(socket_path, name, args, tool_use_id).await { + Ok(result_json) if tool.result_format == ResultFormat::Mcp => { + Some(match McpToolResult::parse(&result_json) { + Ok(result) => result.claude_response(id), + Err(message) => json_rpc_error(id, -32603, message), + }) + } Ok(result_json) => Some(json_rpc_result( id, serde_json::json!({ @@ -1488,7 +1558,7 @@ mod adapter { /// otherwise the connection is parked in a oneshot table and the generic /// request event is emitted exactly once. pub mod mcp_server { - use super::{emit, hook_server, BufReader, HarnessEvent}; + use super::{emit, hook_server, BufReader, HarnessEvent, ResultFormat, ToolManifest}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -1498,12 +1568,19 @@ mod adapter { use tokio::sync::{mpsc, oneshot, Mutex}; pub type ResultsInHand = Arc>>; - pub type ParkedCalls = Arc>>>; + pub type ParkedCalls = Arc>>; + + pub struct PendingCall { + pub tool_name: String, + pub result_format: ResultFormat, + pub sender: Option>, + } #[derive(Clone)] pub struct State { pub results_in_hand: ResultsInHand, pub parked_calls: ParkedCalls, + pub manifest: Arc, pub deferred_calls: hook_server::DeferredCalls, pub current_run_id: hook_server::CurrentRunId, pub evt_tx: mpsc::Sender, @@ -1619,12 +1696,14 @@ mod adapter { let State { results_in_hand, parked_calls, + manifest, deferred_calls, current_run_id, evt_tx, } = state; let (r, mut w) = stream.into_split(); - let Some(line) = BufReader::new(r).lines().next_line().await.ok().flatten() else { + let mut lines = BufReader::new(r).lines(); + let Some(line) = lines.next_line().await.ok().flatten() else { return; }; let request: McpCallRequest = match serde_json::from_str(&line) { @@ -1653,6 +1732,8 @@ mod adapter { calls.remove(&call_id); calls.remove(&stashed_id); drop(calls); + parked.remove(&call_id); + parked.remove(&stashed_id); drop(parked); if stashed_id != call_id { // The stash was keyed by an EARLIER call id (the @@ -1677,7 +1758,18 @@ mod adapter { return; } let (tx, rx) = oneshot::channel(); - parked.insert(call_id.clone(), tx); + parked.insert( + call_id.clone(), + PendingCall { + tool_name: request.name.clone(), + result_format: manifest + .iter() + .find(|tool| tool.name == request.name) + .map(|tool| tool.result_format) + .unwrap_or_default(), + sender: Some(tx), + }, + ); rx }; @@ -1685,7 +1777,7 @@ mod adapter { emit( &evt_tx, HarnessEvent::ToolCallRequested { - run_id, + run_id: run_id.clone(), call_id: call_id.clone(), name: request.name.clone(), args_json: request.args.to_string(), @@ -1693,11 +1785,20 @@ mod adapter { ) .await; - match rx.await { - Ok(result_json) => { - write_result(&mut w, render_result_for_model(&request.name, result_json)).await + tokio::select! { + result = rx => match result { + Ok(result_json) => write_result(&mut w, render_result_for_model(&request.name, result_json)).await, + Err(_) => tracing::debug!(%call_id, "parked MCP call was canceled"), + }, + _ = lines.next_line() => { + // Keep identity after disconnect for late host results. + if let Some(call) = parked_calls.lock().await.get_mut(&call_id) { + call.sender.take(); + } + emit(&evt_tx, HarnessEvent::ToolCallCancelRequested { + run_id, call_id, name: request.name, + }).await; } - Err(_) => tracing::debug!(%call_id, "parked MCP call was cancelled"), } } @@ -1713,8 +1814,9 @@ mod adapter { let _ = writer.flush().await; } - /// Route a host result to a currently parked bridge, or durably stash - /// it for a later id-stable re-fire. Returns true only when a live + /// Route a host result to a currently parked bridge, or stash it in + /// memory for a later id-stable re-fire. The host outbox owns durability. + /// Returns true only when a live /// parked connection accepted the result. pub async fn route_tool_result( call_id: &str, @@ -1723,9 +1825,12 @@ mod adapter { results_in_hand: &ResultsInHand, ) -> bool { let mut parked = parked_calls.lock().await; - if let Some(sender) = parked.remove(call_id) { + if let Some(sender) = parked.get_mut(call_id).and_then(|call| call.sender.take()) { match sender.send(result_json) { - Ok(()) => return true, + Ok(()) => { + parked.remove(call_id); + return true; + } Err(result_json) => { results_in_hand .lock() @@ -1810,24 +1915,12 @@ mod adapter { tokio::fs::write(tracker, format!("{}\n", cwd.display())).await } - /// Write claude's strict MCP config when the manifest contains at least - /// one tool that is not natively bound to Claude. Removing a stale file on - /// the empty path matters on ProcessBackend, where `/workspace` can - /// survive a harness restart with a different manifest. + /// Always limit Claude to the trusted bridge, including an empty manifest. async fn write_mcp_config( path: &Path, - manifest: &ToolManifest, + _manifest: &ToolManifest, self_exe: &Path, ) -> std::io::Result { - if injected_tools(manifest).next().is_none() { - match tokio::fs::remove_file(path).await { - Ok(()) => {} - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => return Err(e), - } - return Ok(false); - } - if let Some(parent) = path.parent() { tokio::fs::create_dir_all(parent).await?; } @@ -1968,20 +2061,15 @@ mod adapter { let self_exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("engram-harness-claude")); let mcp_config_file = state.claude_mcp_config(); - let mcp_config_path = match write_mcp_config( - &mcp_config_file, - &cli.tool_manifest, - &self_exe, - ) - .await - { - Ok(true) => Some(mcp_config_file.as_path()), - Ok(false) => None, - Err(e) => { - tracing::error!(error = %e, "couldn't write claude MCP config; exposing no injected tools"); - None - } - }; + let mcp_config_path = + match write_mcp_config(&mcp_config_file, &cli.tool_manifest, &self_exe).await { + Ok(true) => Some(mcp_config_file.as_path()), + Ok(false) => None, + Err(e) => { + tracing::error!(error = %e, "could not write strict Claude MCP config"); + return ExitCode::FAILURE; + } + }; let hook_sock_path = state.hook_sock(); let _ = tokio::fs::remove_file(&hook_sock_path).await; // clear a stale bind let _sock_guard = match tokio::net::UnixListener::bind(&hook_sock_path) { @@ -2018,6 +2106,7 @@ mod adapter { mcp_server::State { results_in_hand: results_in_hand.clone(), parked_calls: parked_mcp_calls.clone(), + manifest: tool_manifest.clone(), deferred_calls: deferred_calls.clone(), current_run_id: current_run_id.clone(), evt_tx: evt_tx.clone(), @@ -2816,6 +2905,7 @@ mod adapter { if stale_results.is_empty() { HashMap::new() } else { + let mut pending = parked_mcp_calls.lock().await; let mut deferred = deferred_calls.lock().await; stale_results .iter() @@ -2823,6 +2913,7 @@ mod adapter { let tool_name = deferred .remove(call_id) .map(|call| call.tool_name) + .or_else(|| pending.remove(call_id).map(|call| call.tool_name)) .unwrap_or_default(); (call_id.clone(), tool_name) }) @@ -2833,6 +2924,7 @@ mod adapter { let text = fallback_delivery_message( &stale_results, &stale_result_names, + &cli.tool_manifest, ); let ft = start_turn( evt_tx, s, cli, None, &text, current_run_id, @@ -2846,7 +2938,9 @@ mod adapter { // each drained result explicitly or // its coordinator outbox row will // redeliver forever. - for (tool_call_id, _) in stale_results { + for (tool_call_id, result_json) in stale_results { + let ok = external_fallback_result(&tool_call_id, &result_json, &stale_result_names, &cli.tool_manifest) + .is_none_or(|result| result.is_ok_and(|result| result.success())); emit( evt_tx, HarnessEvent::ToolCallCompleted { @@ -2856,7 +2950,7 @@ mod adapter { .cloned() .unwrap_or_default(), tool_call_id, - ok: true, + ok, duration_ms: 0, result_summary: None, }, @@ -3181,6 +3275,12 @@ mod adapter { .await .get(&call_id) .map(|call| call.tool_name.clone()); + let known_deferred = known_tool.is_some(); + let live_context = parked_mcp_calls.lock().await.get(&call_id) + .map(|call| (call.tool_name.clone(), call.result_format)); + let is_mcp = live_context.as_ref().is_some_and(|(_, format)| *format == ResultFormat::Mcp) + || known_tool.as_ref().is_some_and(|name| cli.tool_manifest.iter().any(|tool| tool.name == *name && tool.result_format == ResultFormat::Mcp)); + let known_tool = known_tool.or_else(|| live_context.map(|(name, _)| name)); // ADR 0107 (2026-08-03: injected exit_plan_mode). // BOTH plan decisions take the ordinary deferred // path: stash the result → respawn `--resume` → @@ -3196,8 +3296,8 @@ mod adapter { // `--permission-mode plan`; reject keeps plan for a // read-only revision turn. (The unknown-call_id arm // — post-eviction fresh harness — parses the shape.) - let is_plan_call = known_tool.as_deref() == Some("exit_plan_mode") - || known_tool.is_none(); + let is_plan_call = !is_mcp && (known_tool.as_deref() == Some("exit_plan_mode") + || known_tool.is_none()); if is_plan_call { if let Some(decision) = engram_harness_sdk::plan::parse_plan_decision(&result_json) @@ -3226,7 +3326,6 @@ mod adapter { // bridge until their id-stable resume re-fire, so // every other result is stashed before SIGINT and // consumed only after transcript scrub + --resume. - let known_deferred = known_tool.is_some(); let delivered = mcp_server::route_tool_result( &call_id, result_json, @@ -3631,9 +3730,18 @@ mod adapter { fn fallback_delivery_message( stale_results: &[(String, String)], names: &HashMap, + manifest: &[ManifestTool], ) -> String { let mut lines = vec!["Results for the deferred tool call(s) you made earlier:".to_string()]; for (call_id, result_json) in stale_results { + if let Some(result) = external_fallback_result(call_id, result_json, names, manifest) { + lines.push(format!("- {call_id}:")); + match result { + Ok(result) => lines.extend(result.text_items()), + Err(error) => lines.push(error.into()), + } + continue; + } let plan_decision = (names.get(call_id).map(String::as_str) == Some("exit_plan_mode")) .then(|| engram_harness_sdk::plan::parse_plan_decision(result_json)) .flatten(); @@ -3645,6 +3753,19 @@ mod adapter { lines.join("\n") } + fn external_fallback_result( + call_id: &str, + result_json: &str, + names: &HashMap, + manifest: &[ManifestTool], + ) -> Option> { + let name = names.get(call_id)?; + manifest + .iter() + .find(|tool| tool.name == *name && tool.result_format == ResultFormat::Mcp) + .map(|_| McpToolResult::parse(result_json)) + } + /// Write one prompt to claude's stdin as a stream-json `user` message. /// `serde_json` handles all escaping. async fn write_user_message( @@ -3793,8 +3914,8 @@ mod adapter { // Without strict mode claude merges the guest user's own MCP // configuration, exposing servers outside the orchestrator's // per-session manifest (ADR 0089 spike, Claude 2.1.207). - argv.push("--strict-mcp-config".into()); } + argv.push("--strict-mcp-config".into()); // ADR 0060: an external trigger (e.g. Slack) flavors the agent's system // prompt via ENGRAM_APPEND_SYSTEM_PROMPT (carried through harness_env). // Empty = unset (skip the flag). @@ -4686,10 +4807,10 @@ mod adapter { } #[test] - fn argv_omits_mcp_flags_without_injected_tools() { + fn argv_keeps_strict_mcp_flags_without_injected_tools() { let argv = build_claude_argv(&None, None, None, "default", test_settings()); assert!(!argv.iter().any(|arg| arg == "--mcp-config")); - assert!(!argv.iter().any(|arg| arg == "--strict-mcp-config")); + assert!(argv.iter().any(|arg| arg == "--strict-mcp-config")); } #[tokio::test] @@ -4735,12 +4856,12 @@ mod adapter { ) .unwrap(); assert!( - !write_mcp_config(&path, &native_only, Path::new("/sbin/harness")) + write_mcp_config(&path, &native_only, Path::new("/sbin/harness")) .await .unwrap(), - "a native-only manifest must not enable MCP" + "a native-only manifest must keep the strict bridge config" ); - assert!(tokio::fs::metadata(&path).await.is_err()); + assert!(tokio::fs::metadata(&path).await.is_ok()); let _ = tokio::fs::remove_dir_all(dir).await; } @@ -4894,6 +5015,58 @@ mod adapter { let _ = tokio::fs::remove_file(sock).await; } + #[tokio::test] + async fn external_mcp_bridge_preserves_text_structure_and_upstream_error() { + let dir = + std::env::temp_dir().join(format!("engram-mcp-result-{}", uuid::Uuid::new_v4())); + tokio::fs::create_dir_all(&dir).await.unwrap(); + let sock = dir.join("bridge.sock"); + let listener = tokio::net::UnixListener::bind(&sock).unwrap(); + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let (r, mut w) = stream.into_split(); + let mut lines = BufReader::new(r).lines(); + let _: Value = + serde_json::from_str(&lines.next_line().await.unwrap().unwrap()).unwrap(); + let result = serde_json::json!({ + "outcome":"failed", "delivery":"available", "resultUnavailable":false, + "output": {"content":[{"type":"text","text":"first"},{"type":"text","text":"second"}], + "structuredContent":{"count":2}, "isError":true} + }); + let mut response = + serde_json::to_vec(&serde_json::json!({"result_json":result.to_string()})) + .unwrap(); + response.push(b'\n'); + w.write_all(&response).await.unwrap(); + }); + let manifest = parse_tool_manifest(r#"[{"name":"mcp_inventory_01234567890123456789","description":"Read inventory","inputSchema":{"type":"object"},"execution":"sync","nativeBindings":{},"resultFormat":"mcp"}]"#).unwrap(); + let response = mcp_bridge::handle_request( + serde_json::json!({"jsonrpc":"2.0","id":1,"method":"tools/call", + "params":{"name":"mcp_inventory_01234567890123456789","arguments":{}}}), + &manifest, + &sock, + ) + .await + .unwrap(); + assert_eq!( + response["result"]["content"], + serde_json::json!([ + {"type":"text","text":"first"}, {"type":"text","text":"second"} + ]) + ); + assert_eq!( + response["result"]["structuredContent"], + serde_json::json!({"count":2}) + ); + assert_eq!(response["result"]["isError"], true); + assert!( + response.get("error").is_none(), + "an upstream tool error is not a delivery error" + ); + server.await.unwrap(); + tokio::fs::remove_dir_all(dir).await.unwrap(); + } + async fn spawn_main_mcp_server() -> ( String, mcp_server::ResultsInHand, @@ -4922,6 +5095,7 @@ mod adapter { mcp_server::State { results_in_hand: results.clone(), parked_calls: parked.clone(), + manifest: Arc::new(Vec::new()), deferred_calls: deferred.clone(), current_run_id: run_id.clone(), evt_tx, @@ -5000,6 +5174,104 @@ mod adapter { let _ = tokio::fs::remove_file(sock).await; } + #[tokio::test] + async fn main_mcp_disconnect_requests_cancel_with_original_identity() { + let (sock, results, parked, mut events, _deferred) = spawn_main_mcp_server().await; + let mut stream = tokio::net::UnixStream::connect(&sock).await.unwrap(); + stream.write_all(b"{\"name\":\"mcp_inventory_01234567890123456789\",\"args\":{},\"tool_use_id\":\"call-cancel\"}\n").await.unwrap(); + let requested = tokio::time::timeout(Duration::from_secs(1), events.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(requested.kind(), "tool_call_requested"); + drop(stream); + let canceled = tokio::time::timeout(Duration::from_secs(1), events.recv()) + .await + .expect("disconnect must request cancellation") + .unwrap(); + assert_eq!(canceled.kind(), "tool_call_cancel_requested"); + assert_eq!(canceled.run_id(), Some("run-mcp")); + assert_eq!(canceled.tool_call_id(), Some("call-cancel")); + assert_eq!( + serde_json::to_value(&canceled).unwrap()["ToolCallCancelRequested"]["name"], + "mcp_inventory_01234567890123456789" + ); + assert!(parked + .lock() + .await + .get("call-cancel") + .unwrap() + .sender + .is_none()); + assert_eq!( + parked.lock().await.get("call-cancel").unwrap().tool_name, + "mcp_inventory_01234567890123456789" + ); + // A late journal result is retained. Cancellation is not completion. + assert!( + !mcp_server::route_tool_result("call-cancel", "late".into(), &parked, &results) + .await + ); + assert_eq!( + results.lock().await.get("call-cancel").map(String::as_str), + Some("late") + ); + tokio::fs::remove_file(sock).await.unwrap(); + } + + #[tokio::test] + async fn external_mcp_native_cancel_is_processed_while_call_is_pending() { + let (sock, _results, _parked, mut events, _deferred) = spawn_main_mcp_server().await; + let manifest = parse_tool_manifest(r#"[{"name":"remote_inventory","description":"Read","inputSchema":{"type":"object"},"execution":"sync","nativeBindings":{},"resultFormat":"mcp"}]"#).unwrap(); + let (client, bridge) = tokio::io::duplex(8192); + let (client_read, mut client_write) = tokio::io::split(client); + let (bridge_read, bridge_write) = tokio::io::split(bridge); + let bridge_sock = sock.clone(); + let task = tokio::spawn(async move { + mcp_bridge::serve_io( + BufReader::new(bridge_read), + bridge_write, + &manifest, + Path::new(&bridge_sock), + ) + .await + }); + client_write.write_all(b"{\"jsonrpc\":\"2.0\",\"id\":17,\"method\":\"tools/call\",\"params\":{\"name\":\"remote_inventory\",\"arguments\":{},\"_meta\":{\"claudecode/toolUseId\":\"native-cancel\"}}}\n").await.unwrap(); + let requested = tokio::time::timeout(Duration::from_secs(1), events.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(requested.tool_call_id(), Some("native-cancel")); + client_write.write_all(b"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/cancelled\",\"params\":{\"requestId\":17}}\n{\"jsonrpc\":\"2.0\",\"id\":18,\"method\":\"tools/list\"}\n").await.unwrap(); + let mut responses = BufReader::new(client_read).lines(); + let response = tokio::time::timeout(Duration::from_secs(1), responses.next_line()) + .await + .expect("pending call must not block cancellation or tools/list") + .unwrap() + .unwrap(); + let response: Value = serde_json::from_str(&response).unwrap(); + assert_eq!( + response["id"], 18, + "canceled request must not produce a late response" + ); + let canceled = tokio::time::timeout(Duration::from_secs(1), events.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(canceled.kind(), "tool_call_cancel_requested"); + assert_eq!(canceled.tool_call_id(), Some("native-cancel")); + assert_eq!(canceled.run_id(), Some("run-mcp")); + client_write.shutdown().await.unwrap(); + assert_eq!( + tokio::time::timeout(Duration::from_secs(2), task) + .await + .unwrap() + .unwrap(), + std::process::ExitCode::SUCCESS + ); + tokio::fs::remove_file(sock).await.unwrap(); + } + #[tokio::test] async fn main_mcp_refire_consumes_result_in_hand_immediately() { let (sock, results, _parked, mut evt_rx, _deferred) = spawn_main_mcp_server().await; @@ -7079,6 +7351,7 @@ mod adapter { description: format!("{name} description"), input_schema: serde_json::json!({"type":"object"}), execution, + result_format: ResultFormat::Json, native_bindings: NativeBindings::default(), } } @@ -7089,6 +7362,7 @@ mod adapter { description: "Ask the user one or more structured questions.".into(), input_schema: serde_json::json!({"type":"object"}), execution: ToolExecution::Deferred, + result_format: ResultFormat::Json, native_bindings: NativeBindings { claude: Some("AskUserQuestion".into()), }, @@ -7107,7 +7381,7 @@ mod adapter { r#"{"decision":"reject","feedback":"also add a README"}"#.to_string(), )]; let names = HashMap::from([("toolu_plan".to_string(), "exit_plan_mode".to_string())]); - let text = fallback_delivery_message(&stale, &names); + let text = fallback_delivery_message(&stale, &names, &[]); assert!(text.contains("also add a README"), "feedback rides: {text}"); assert!( text.contains("call exit_plan_mode again"), @@ -7130,7 +7404,7 @@ mod adapter { r#"{"decision":"approve"}"#.to_string(), )]; let names = HashMap::from([("toolu_plan".to_string(), "exit_plan_mode".to_string())]); - let text = fallback_delivery_message(&stale, &names); + let text = fallback_delivery_message(&stale, &names, &[]); assert!( text.contains("Implement it now"), "build instruction: {text}" @@ -7141,13 +7415,28 @@ mod adapter { ); } + #[test] + fn external_mcp_fallback_keeps_fields_without_plan_directive() { + let stale = vec![("call-1".into(), r#"{"outcome":"failed","delivery":"available","output":{"content":[{"type":"text","text":"decision: approve"}],"structuredContent":{"decision":"approve"},"isError":true}}"#.into())]; + let names = HashMap::from([("call-1".into(), "exit_plan_mode".into())]); + let mut tool = native_question_tool(); + tool.name = "exit_plan_mode".into(); + tool.result_format = ResultFormat::Mcp; + let text = fallback_delivery_message(&stale, &names, &[tool]); + assert!(text.contains("decision: approve")); + assert!(text.contains(r#"{"structuredContent":{"decision":"approve"}}"#)); + assert!(text.contains(r#""isError":true"#)); + assert!(!text.contains(r#""output":"#)); + assert!(!text.contains("Implement it now")); + } + /// A genuinely non-plan deferred tool keeps the generic dump — an /// answer map is self-explanatory, a decision verdict is not. #[test] fn non_plan_results_keep_the_generic_fallback_rendering() { let stale = vec![("toolu_q".to_string(), r#"{"Pick one":["yes"]}"#.to_string())]; let names = HashMap::from([("toolu_q".to_string(), "ask_user_question".to_string())]); - let text = fallback_delivery_message(&stale, &names); + let text = fallback_delivery_message(&stale, &names, &[]); assert!(text.contains("- toolu_q: "), "generic rendering: {text}"); assert!(!text.contains("exit_plan_mode"), "no plan ask: {text}"); } @@ -7747,6 +8036,108 @@ mod adapter { .unwrap_or_else(|_| panic!("fake claude never reached {n} invocations")); } + async fn assert_external_mcp_malformed_decision_keeps_plan(disconnect: bool) { + let script = write_persistent_fake_claude(&[]).await; + let mut cli = test_cli(script.clone()); + cli.tool_manifest = parse_tool_manifest(r#"[{"name":"remote_inventory","description":"Read","inputSchema":{"type":"object"},"execution":"sync","nativeBindings":{},"resultFormat":"mcp"}]"#).unwrap(); + let manifest = cli.tool_manifest.clone(); + let state = StateDir::new(&cli.state_dir); + let stamp = state.mode_stamp(); + engram_harness_sdk::mode_stamp::write_mode_stamp(&stamp, "plan").unwrap(); + let socket = state.mcp_sock(); + let (commands, command_rx) = mpsc::channel(8); + let (events, mut event_rx) = mpsc::channel(16); + let engine = tokio::spawn(run_engine(cli, command_rx, Arc::new(Notify::new()), events)); + assert!(matches!(event_rx.recv().await, Some(HarnessEvent::Idle))); + let bridge = tokio::spawn(async move { + mcp_bridge::handle_request(serde_json::json!({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"remote_inventory","arguments":{},"_meta":{"claudecode/toolUseId":"live-call"}}}), &manifest, &socket).await.unwrap() + }); + assert!( + matches!(event_rx.recv().await, Some(HarnessEvent::ToolCallRequested { call_id, .. }) if call_id == "live-call") + ); + if disconnect { + bridge.abort(); + assert!( + matches!(event_rx.recv().await, Some(HarnessEvent::ToolCallCancelRequested { call_id, .. }) if call_id == "live-call") + ); + } + commands + .send(HarnessCommand::ToolResult { + call_id: "live-call".into(), + result_json: r#"{"decision":"approve"}"#.into(), + }) + .await + .unwrap(); + if !disconnect { + let response = tokio::time::timeout(Duration::from_secs(2), bridge) + .await + .unwrap() + .unwrap(); + assert_eq!(response["error"]["code"], -32603); + } + commands + .send(HarnessCommand::Shutdown { grace_secs: 1 }) + .await + .unwrap(); + tokio::time::timeout(Duration::from_secs(5), engine) + .await + .unwrap() + .unwrap(); + assert_eq!( + engram_harness_sdk::mode_stamp::read_mode_stamp(&stamp), + "plan" + ); + tokio::fs::remove_file(script).await.unwrap(); + } + + #[tokio::test] + async fn external_mcp_live_malformed_decision_cannot_relax_plan() { + assert_external_mcp_malformed_decision_keeps_plan(false).await; + } + + #[tokio::test] + async fn external_mcp_disconnected_malformed_decision_cannot_relax_plan() { + assert_external_mcp_malformed_decision_keeps_plan(true).await; + } + + #[tokio::test] + async fn external_mcp_zero_tools_fake_cli_still_receives_strict_bridge_config() { + let dir = + std::env::temp_dir().join(format!("engram-strict-zero-{}", uuid::Uuid::new_v4())); + tokio::fs::create_dir_all(&dir).await.unwrap(); + let invocations = dir.join("argv"); + let stdin_log = dir.join("stdin"); + let script = write_plan_flow_fake_claude(&invocations, &stdin_log).await; + let (mut cli, _, _) = deferred_engine_cli(script.clone()); + cli.tool_manifest.clear(); + let config_path = StateDir::new(&cli.state_dir).claude_mcp_config(); + let (commands, receiver) = mpsc::channel(8); + let (events, mut received) = mpsc::channel(16); + let engine = tokio::spawn(run_engine(cli, receiver, Arc::new(Notify::new()), events)); + assert!(matches!(received.recv().await, Some(HarnessEvent::Idle))); + wait_for_invocations(&invocations, 1).await; + let argv = tokio::fs::read_to_string(invocations).await.unwrap(); + assert!(argv.contains("--strict-mcp-config")); + assert!(argv.contains(&format!("--mcp-config {}", config_path.display()))); + let config: Value = + serde_json::from_slice(&tokio::fs::read(&config_path).await.unwrap()).unwrap(); + assert_eq!(config["mcpServers"].as_object().unwrap().len(), 1); + assert_eq!( + config["mcpServers"]["engrams"]["args"], + serde_json::json!(["mcp-bridge"]) + ); + commands + .send(HarnessCommand::Shutdown { grace_secs: 1 }) + .await + .unwrap(); + tokio::time::timeout(Duration::from_secs(5), engine) + .await + .unwrap() + .unwrap(); + tokio::fs::remove_file(script).await.unwrap(); + tokio::fs::remove_dir_all(dir).await.unwrap(); + } + /// `exit_plan_mode` as production ships it since PR #981: an INJECTED /// deferred tool (no claude native binding), reached as /// `mcp__engrams__exit_plan_mode`. The old `native_bindings.claude = @@ -7758,6 +8149,7 @@ mod adapter { description: "Present your finished implementation plan.".into(), input_schema: serde_json::json!({"type":"object"}), execution: ToolExecution::Deferred, + result_format: ResultFormat::Json, native_bindings: NativeBindings::default(), } } @@ -8298,8 +8690,7 @@ mod adapter { let _ = tokio::fs::remove_file(counter).await; } - #[tokio::test] - async fn deferred_result_after_fresh_restore_resumes_and_serves_refire() { + async fn assert_fresh_restore_refire(external: bool) { let nonce = uuid::Uuid::new_v4(); let counter = std::env::temp_dir().join(format!("fake-claude-count-{nonce}")); let invocations = std::env::temp_dir().join(format!("fake-claude-invocations-{nonce}")); @@ -8313,7 +8704,21 @@ mod adapter { &session_id, ) .await; - let (cli, hook, mcp) = deferred_engine_cli(script.clone()); + let (mut cli, hook, mcp) = deferred_engine_cli(script.clone()); + if external { + cli.tool_manifest[0].result_format = ResultFormat::Mcp; + } + let manifest = cli.tool_manifest.clone(); + let mode_stamp = StateDir::new(&cli.state_dir).mode_stamp(); + engram_harness_sdk::mode_stamp::write_mode_stamp(&mode_stamp, "plan").unwrap(); + let result_json = if external { + serde_json::json!({"outcome":"succeeded","delivery":"available","output":{ + "content":[{"type":"text","text":"{\"decision\":\"approve\"}"},{"type":"text","text":"second"}], + "structuredContent":{"decision":"approve","count":2},"isError":false + },"resultUnavailable":false}).to_string() + } else { + r#"{"saved":true}"#.to_string() + }; let stash = StateDir::new(&cli.state_dir).claude_session_id(); let (cmd_tx, cmd_rx) = mpsc::channel(8); let (evt_tx, mut evt_rx) = mpsc::channel(64); @@ -8340,7 +8745,7 @@ mod adapter { cmd_tx .send(HarnessCommand::ToolResult { call_id: "toolu_restored".into(), - result_json: r#"{"saved":true}"#.into(), + result_json: result_json.clone(), }) .await .unwrap(); @@ -8380,10 +8785,32 @@ mod adapter { .await, hook_server::HookVerdict::Allow )); - assert_eq!( - fire_main_mcp_call(&mcp, "toolu_restored").await["result_json"], - r#"{"saved":true}"# - ); + let response = mcp_bridge::handle_request( + serde_json::json!({ + "jsonrpc":"2.0","id":1,"method":"tools/call","params":{ + "name":"save_memory","arguments":{"text":"remember"}, + "_meta":{"claudecode/toolUseId":"toolu_restored"} + } + }), + &manifest, + &mcp, + ) + .await + .unwrap(); + if external { + assert_eq!( + response["result"]["structuredContent"], + serde_json::json!({"decision":"approve","count":2}) + ); + assert_eq!(response["result"]["content"].as_array().unwrap().len(), 2); + assert_eq!(response["result"]["isError"], false); + assert_eq!( + engram_harness_sdk::mode_stamp::read_mode_stamp(&mode_stamp), + "plan" + ); + } else { + assert_eq!(response["result"]["content"][0]["text"], result_json); + } tokio::fs::write(&completion_gate, b"served").await.unwrap(); let mut continued = false; @@ -8424,6 +8851,16 @@ mod adapter { let _ = tokio::fs::remove_file(completion_gate).await; } + #[tokio::test] + async fn deferred_result_after_fresh_restore_resumes_and_serves_refire() { + assert_fresh_restore_refire(false).await; + } + + #[tokio::test] + async fn external_mcp_fresh_engine_refire_preserves_envelope_and_plan_latch() { + assert_fresh_restore_refire(true).await; + } + #[tokio::test] async fn deferred_result_with_live_process_resumes_and_serves_refire() { let counter = @@ -8660,7 +9097,7 @@ mod adapter { } } -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", test))] #[tokio::main] async fn main() -> std::process::ExitCode { // ADR 0054: git/busybox argv dispatch — the SAME binary is the socket @@ -8676,7 +9113,7 @@ async fn main() -> std::process::ExitCode { adapter::entry().await } -#[cfg(all(test, target_os = "linux"))] +#[cfg(test)] mod tests { use super::adapter::*; use engram_harness_proto::{FileChange, HarnessEvent}; @@ -9327,6 +9764,7 @@ mod tests { description: "Save a memory".into(), input_schema: serde_json::json!({"type":"object"}), execution: ToolExecution::Deferred, + result_format: engram_harness_proto::tool_result::ResultFormat::Json, native_bindings: NativeBindings::default(), }]; let deferred_names = deferred_tool_names(&manifest); @@ -9410,6 +9848,7 @@ mod tests { description: "Present the plan".into(), input_schema: serde_json::json!({"type":"object"}), execution: ToolExecution::Deferred, + result_format: engram_harness_proto::tool_result::ResultFormat::Json, native_bindings: NativeBindings { claude: Some("ExitPlanMode".into()), }, diff --git a/crates/engram-harness-codex/src/main.rs b/crates/engram-harness-codex/src/main.rs index 75436fbbf..4bb3155c1 100644 --- a/crates/engram-harness-codex/src/main.rs +++ b/crates/engram-harness-codex/src/main.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use clap::Parser; use engram_core::SessionId; +use engram_harness_proto::tool_result::{McpToolResult, ResultFormat}; use engram_harness_proto::{ read_msg, write_msg, AgentRole, FileChange, ForgeOp, ForgeRequest, ForgeResponse, HarnessCommand, HarnessEvent, FORGE_VSOCK_PORT, @@ -31,6 +32,7 @@ struct ManifestTool { description: String, input_schema: Value, execution: ToolExecution, + result_format: ResultFormat, codex_native_binding: Option, } @@ -82,6 +84,12 @@ fn parse_tool_manifest(raw: &str) -> Result { .cloned() .ok_or("ENGRAM_TOOLS entry missing inputSchema")?, execution, + result_format: object + .get("resultFormat") + .map(|value| serde_json::from_value(value.clone())) + .transpose() + .map_err(|error| error.to_string())? + .unwrap_or_default(), codex_native_binding, }) }) @@ -113,6 +121,7 @@ fn with_browser_view(mut manifest: ToolManifest, enabled: bool) -> ToolManifest description: browser_view::TOOL_DESCRIPTION.into(), input_schema: browser_view::input_schema(), execution: ToolExecution::Sync, + result_format: ResultFormat::Json, codex_native_binding: None, }); } @@ -487,6 +496,7 @@ enum FollowUpCompletion { Tool { name: String, result_summary: String, + ok: bool, }, } @@ -1092,6 +1102,7 @@ async fn drive( summary: Some(engram_harness_sdk::truncate_utf8(&prompt.text, 1024)), }).await; queued.push_back(prompt); + request_tool_cancellations(parked, events, Some(&turn_id)).await; interrupt_turn(server, &mut pending, &mut interrupt, &turn_id, "mode change").await; } else if let Some(turn_id) = active.as_deref() { let id = server.send_request("turn/steer", json!({ @@ -1130,6 +1141,7 @@ async fn drive( } } Some(HarnessCommand::Interrupt) => { + request_tool_cancellations(parked, events, active.as_deref()).await; if let Some(turn_id) = active.clone() { interrupt_turn(server, &mut pending, &mut interrupt, &turn_id, "operator interrupt").await; } @@ -1150,7 +1162,10 @@ async fn drive( ) .await; } - Some(HarnessCommand::Shutdown { .. }) => return DriveOutcome::Shutdown, + Some(HarnessCommand::Shutdown { .. }) => { + request_tool_cancellations(parked, events, None).await; + return DriveOutcome::Shutdown; + }, Some(HarnessCommand::Checkpoint { .. }) => {} None => { if let Some(run_id) = active.take() { @@ -1408,7 +1423,7 @@ async fn handle_dynamic_tool_call( request_id, tool_name, server.generation, - json!({"execution":execution}), + json!({"execution":execution,"resultFormat":tool.result_format,"runId":params.get("turnId").and_then(Value::as_str).or(active.as_deref()).unwrap_or("")}), ); if let Err(error) = parked.record(call) { tracing::error!(%error, %call_id, "could not durably park Codex dynamic tool call"); @@ -1483,7 +1498,41 @@ impl InterruptWatchdog { } } -/// Ask codex to end `turn_id` and start the clock. The ONLY way to interrupt. +/// A whole-turn interrupt is the only confirmed native cancellation signal. +/// Retain the parked record so a late journal result can still be delivered. +async fn request_tool_cancellations( + parked: &ParkedCallStore, + events: &mpsc::Sender, + run_id: Option<&str>, +) { + for call in parked.all() { + if call.context.get("resultFormat").and_then(Value::as_str) != Some("mcp") { + continue; + } + let Some(original_run) = call + .context + .get("runId") + .and_then(Value::as_str) + .filter(|run| !run.is_empty()) + else { + continue; + }; + if run_id.is_some_and(|run| run != original_run) { + continue; + } + emit( + events, + HarnessEvent::ToolCallCancelRequested { + run_id: original_run.into(), + call_id: call.tool_call_id, + name: call.tool_name, + }, + ) + .await; + } +} + +/// Ask Codex to end `turn_id` and start the interrupt clock. async fn interrupt_turn( server: &mut AppServer, pending: &mut HashMap, @@ -1528,11 +1577,16 @@ async fn route_tool_result( tracing::error!(%call_id, kind = ?call.kind, "ToolResult does not match a dynamic tool call"); return; } + let is_mcp = call.context.get("resultFormat").and_then(Value::as_str) == Some("mcp"); + let mcp_result = is_mcp.then(|| McpToolResult::parse(&result_json)); + let result_ok = mcp_result + .as_ref() + .is_none_or(|result| result.as_ref().is_ok_and(McpToolResult::success)); // ADR 0107: an approved plan flips the stamp FIRST, so every turn that // starts after this point (the live build turn below, or the // stale-generation follow-up) reads full access. A reject leaves the plan // stamp in place, so its revision turn stays read-only. - let plan_decision = (call.tool_name == "exit_plan_mode") + let plan_decision = (call.tool_name == "exit_plan_mode" && !is_mcp) .then(|| engram_harness_sdk::plan::parse_plan_decision(&result_json)) .flatten(); // ADR 0107: a REJECT ends the plan turn and starts a new one carrying the @@ -1592,6 +1646,7 @@ async fn route_tool_result( let completion = FollowUpCompletion::Tool { name: call.tool_name.clone(), result_summary: result_json, + ok: false, }; if let Err(error) = send_follow_up(server, active, pending, call_id, text, completion).await { @@ -1662,6 +1717,7 @@ async fn route_tool_result( let completion = FollowUpCompletion::Tool { name: call.tool_name.clone(), result_summary: result_json, + ok: true, }; if let Err(error) = send_follow_up( server, @@ -1677,8 +1733,8 @@ async fn route_tool_result( } return; } - let native_question = - call.context.get("nativeBinding").and_then(Value::as_str) == Some("requestUserInput"); + let native_question = !is_mcp + && call.context.get("nativeBinding").and_then(Value::as_str) == Some("requestUserInput"); let answers = if native_question { match serde_json::from_str::(&result_json) { Ok(answers) => Some(answers), @@ -1691,7 +1747,16 @@ async fn route_tool_result( None }; if call.request_generation != server.generation || call.request_id.is_null() { - let message = if native_question { + let message = if let Some(result) = &mcp_result { + let text = match result { + Ok(result) => result.text_items().join("\n\n"), + Err(message) => (*message).to_string(), + }; + format!( + "Result of the earlier external tool call `{}` ({call_id}):\n{text}", + call.tool_name + ) + } else if native_question { format!( "Answer to the earlier question ({call_id}) after Codex restarted: {result_json}" ) @@ -1704,6 +1769,7 @@ async fn route_tool_result( let completion = FollowUpCompletion::Tool { name: call.tool_name.clone(), result_summary: result_json, + ok: result_ok, }; if let Err(error) = send_follow_up(server, active, pending, call_id, message, completion).await @@ -1724,10 +1790,14 @@ async fn route_tool_result( } else if let Err(error) = server .respond( call.request_id, - json!({ - "success":true, - "contentItems":[{"type":"inputText","text":result_json}] - }), + if let Some(result) = &mcp_result { + result + .as_ref() + .map(|result| result.codex_response()) + .unwrap_or_else(|_| engram_harness_proto::tool_result::invalid_codex_result()) + } else { + json!({"success":true,"contentItems":[{"type":"inputText","text":result_json}]}) + }, ) .await { @@ -1970,6 +2040,7 @@ async fn handle_message( FollowUpCompletion::Tool { name, result_summary, + ok, } => { emit( events, @@ -1977,7 +2048,7 @@ async fn handle_message( run_id: active.clone().unwrap_or_default(), tool_call_id: call_id, tool_name: name, - ok: true, + ok, duration_ms: 0, result_summary: Some(engram_harness_sdk::truncate_utf8( &result_summary, @@ -2082,6 +2153,7 @@ async fn handle_message( .await; } if status == "interrupted" { + request_tool_cancellations(tools.parked, events, Some(&completed)).await; emit( events, HarnessEvent::RunInterrupted { @@ -3063,7 +3135,8 @@ done let manifest = r#"[ {"name":"save_memory","description":"Save a memory","inputSchema":{"type":"object","properties":{"text":{"type":"string"}}},"execution":"sync","nativeBindings":{}}, {"name":"ask_user_question","description":"Ask the user","inputSchema":{"type":"object"},"execution":"deferred","nativeBindings":{"codex":"requestUserInput"}}, - {"name":"claude_native","description":"Only native on Claude","inputSchema":{"type":"object"},"execution":"sync","nativeBindings":{"claude":"Example"}} + {"name":"claude_native","description":"Only native on Claude","inputSchema":{"type":"object"},"execution":"sync","nativeBindings":{"claude":"Example"}}, + {"name":"remote_inventory","description":"Inventory","inputSchema":{"type":"object","properties":{"code":{"type":"string","enum":["a","b"]}},"required":["code"],"additionalProperties":false},"execution":"sync","nativeBindings":{},"resultFormat":"mcp"} ]"#; std::env::set_var("ENGRAM_TOOLS", manifest); let (script, record) = write_fake_codex(&[]).await; @@ -3095,6 +3168,11 @@ done "name":"claude_native", "description":"Only native on Claude", "inputSchema":{"type":"object"} + }, + { + "name":"remote_inventory", + "description":"Inventory", + "inputSchema":{"type":"object","properties":{"code":{"type":"string","enum":["a","b"]}},"required":["code"],"additionalProperties":false} } ])) ); @@ -3520,6 +3598,201 @@ done engine.abort(); } + async fn assert_external_mcp_dynamic_result( + result_json: &str, + native_result: Value, + deferred: bool, + ) { + let tool_call = r#"{"id":77,"method":"item/tool/call","params":{"callId":"call-1","tool":"mcp_inventory_01234567890123456789","arguments":{"text":"remember this"},"threadId":"t1","turnId":"turn-1"}}"#; + let (script, record) = write_fake_codex(&[tool_call]).await; + let mut cli = test_cli(script); + let stamp = cli.state().mode_stamp(); + cli.tool_manifest = parse_tool_manifest( + r#"[{"name":"mcp_inventory_01234567890123456789","description":"Save a memory","inputSchema":{"type":"object"},"execution":"sync","nativeBindings":{},"resultFormat":"mcp"}]"#, + ) + .unwrap(); + if deferred { + cli.tool_manifest[0].execution = ToolExecution::Deferred; + } + let (command_tx, command_rx) = mpsc::channel(8); + let (event_tx, mut event_rx) = mpsc::channel(16); + let engine = tokio::spawn(run_engine( + cli, + command_rx, + Arc::new(Notify::new()), + event_tx, + )); + command_tx + .send(HarnessCommand::Prompt { + prompt_id: "prompt-1".into(), + text: "remember".into(), + mode: Some("plan".into()), + }) + .await + .unwrap(); + + let event = tokio::time::timeout(std::time::Duration::from_secs(3), async { + loop { + if let Some(HarnessEvent::ToolCallRequested { + run_id, + call_id, + name, + args_json, + }) = event_rx.recv().await + { + break (run_id, call_id, name, args_json); + } + } + }) + .await + .expect("dynamic tool request timed out"); + assert_eq!( + event, + ( + "turn-1".into(), + "call-1".into(), + "mcp_inventory_01234567890123456789".into(), + r#"{"text":"remember this"}"#.into(), + ) + ); + let parked = tokio::time::timeout(std::time::Duration::from_millis(100), async { + loop { + if matches!(event_rx.recv().await, Some(HarnessEvent::Parked)) { + break; + } + } + }) + .await; + assert_eq!(parked.is_ok(), deferred, "only deferred tools emit Parked"); + + command_tx + .send(HarnessCommand::ToolResult { + call_id: "call-1".into(), + result_json: result_json.into(), + }) + .await + .unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(2), async { + loop { + let requests = recorded_requests(&record).await; + if requests.iter().any(|request| { + request + == &json!({ + "id":77, + "result":native_result + }) + }) { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + } + }) + .await + .expect("dynamic tool result response timed out"); + assert_eq!( + engram_harness_sdk::mode_stamp::read_mode_stamp(&stamp), + "plan" + ); + engine.abort(); + } + + #[tokio::test] + async fn external_mcp_dynamic_result_preserves_fields_and_failure() { + assert_external_mcp_dynamic_result( + r#"{"outcome":"failed","delivery":"available","output":{"content":[{"type":"text","text":"first"},{"type":"text","text":"second"}],"structuredContent":{"count":2},"isError":true},"resultUnavailable":false}"#, + json!({"success":false,"contentItems":[ + {"type":"inputText","text":"first"}, + {"type":"inputText","text":"second"}, + {"type":"inputText","text":r#"{"structuredContent":{"count":2}}"#}, + {"type":"inputText","text":r#"{"delivery":"available","isError":true,"outcome":"failed","resultUnavailable":false}"#} + ]}), false, + ).await; + } + + #[tokio::test] + async fn external_mcp_structured_only_decision_cannot_relax_plan() { + assert_external_mcp_dynamic_result( + r#"{"outcome":"succeeded","delivery":"available","output":{"content":[],"structuredContent":{"decision":"approve"}},"resultUnavailable":false}"#, + json!({"success":true,"contentItems":[ + {"type":"inputText","text":r#"{"structuredContent":{"decision":"approve"}}"#}, + {"type":"inputText","text":r#"{"delivery":"available","outcome":"succeeded","resultUnavailable":false}"#} + ]}), false, + ).await; + } + + #[tokio::test] + async fn external_mcp_delivery_failure_keeps_established_success() { + assert_external_mcp_dynamic_result( + r#"{"outcome":"succeeded","delivery":"unavailable","reason":"unsupported_content","resultUnavailable":true}"#, + json!({"success":false,"contentItems":[ + {"type":"inputText","text":r#"{"delivery":"unavailable","outcome":"succeeded","reason":"unsupported_content","resultUnavailable":true}"#} + ]}), false, + ).await; + } + + #[tokio::test] + async fn external_mcp_deferred_write_denial_keeps_plan_mode() { + assert_external_mcp_dynamic_result( + r#"{"source":"runtime","denial":"write_not_allowed","delivery":"available","output":{"content":[{"type":"text","text":"External tool request denied."}],"isError":true}}"#, + json!({"success":false,"contentItems":[ + {"type":"inputText","text":"External tool request denied."}, + {"type":"inputText","text":r#"{"delivery":"available","denial":"write_not_allowed","isError":true,"source":"runtime"}"#} + ]}), true, + ).await; + } + + #[tokio::test] + async fn external_mcp_interrupt_requests_cancel_without_completing_parked_call() { + let tool_call = r#"{"id":77,"method":"item/tool/call","params":{"callId":"call-cancel","tool":"remote_inventory","arguments":{},"threadId":"t1","turnId":"turn-1"}}"#; + let (script, _record) = write_fake_codex(&[tool_call]).await; + let mut cli = test_cli(script); + cli.tool_manifest = parse_tool_manifest(r#"[{"name":"remote_inventory","description":"Inventory","inputSchema":{"type":"object"},"execution":"sync","nativeBindings":{},"resultFormat":"mcp"}]"#).unwrap(); + let parked_path = cli.parked_calls_file(); + let (commands, receiver) = mpsc::channel(8); + let (events, mut received) = mpsc::channel(16); + let engine = tokio::spawn(run_engine(cli, receiver, Arc::new(Notify::new()), events)); + commands + .send(HarnessCommand::Prompt { + prompt_id: "p-cancel".into(), + text: "read".into(), + mode: None, + }) + .await + .unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(3), async { + while let Some(event) = received.recv().await { + if event.kind() == "tool_call_requested" { + break; + } + } + }) + .await + .unwrap(); + commands.send(HarnessCommand::Interrupt).await.unwrap(); + let cancel = tokio::time::timeout(std::time::Duration::from_secs(3), async { + while let Some(event) = received.recv().await { + assert_ne!( + event.kind(), + "tool_call_completed", + "interrupt cannot claim completion" + ); + if event.kind() == "tool_call_cancel_requested" { + return event; + } + } + panic!("event stream ended"); + }) + .await + .expect("interrupt must request cancellation"); + assert_eq!(cancel.run_id(), Some("turn-1")); + assert_eq!(cancel.tool_call_id(), Some("call-cancel")); + assert!(ParkedCallStore::open(parked_path) + .unwrap() + .get("call-cancel") + .is_some()); + engine.abort(); + } + #[tokio::test] async fn native_request_user_input_uses_generic_frames_and_is_parked_durably() { let question = r#"{"id":88,"method":"item/tool/requestUserInput","params":{"itemId":"question-1","threadId":"t1","turnId":"turn-1","questions":[{"id":"q1","question":"Deploy now?","header":"Deploy","multiSelect":false,"options":[{"label":"Yes","description":"Deploy it"}]}]}}"#; @@ -3768,6 +4041,111 @@ done assert_eq!(exit, ExitCode::SUCCESS); } + #[tokio::test] + async fn external_mcp_cold_reopen_preserves_failed_result_and_plan_latch() { + let tool_call = r#"{"id":77,"method":"item/tool/call","params":{"callId":"cold-call","tool":"remote_inventory","arguments":{},"threadId":"t1","turnId":"turn-1"}}"#; + let (script, _) = write_fake_codex(&[tool_call]).await; + let mut cli = test_cli(script); + cli.tool_manifest = parse_tool_manifest(r#"[{"name":"remote_inventory","description":"Read","inputSchema":{"type":"object"},"execution":"deferred","nativeBindings":{},"resultFormat":"mcp"}]"#).unwrap(); + let manifest = cli.tool_manifest.clone(); + let state_dir = cli.state_dir.clone(); + let parked_path = cli.parked_calls_file(); + let stamp = cli.state().mode_stamp(); + let (commands, receiver) = mpsc::channel(8); + let (events, mut received) = mpsc::channel(16); + let engine = tokio::spawn(run_engine(cli, receiver, Arc::new(Notify::new()), events)); + commands + .send(HarnessCommand::Prompt { + prompt_id: "cold-prompt".into(), + text: "read".into(), + mode: Some("plan".into()), + }) + .await + .unwrap(); + tokio::time::timeout(std::time::Duration::from_secs(3), async { + while let Some(event) = received.recv().await { + if event.kind() == "tool_call_requested" { + return; + } + } + panic!("missing original call"); + }) + .await + .unwrap(); + engine.abort(); + let _ = engine.await; + let persisted = ParkedCallStore::open(&parked_path).unwrap(); + let call = persisted.get("cold-call").unwrap(); + assert_eq!(call.tool_name, "remote_inventory"); + assert_eq!(call.context["resultFormat"], "mcp"); + assert_eq!(call.context["runId"], "turn-1"); + drop(persisted); + let (script, record) = write_fake_codex(&[]).await; + let mut restored = test_cli(script); + restored.state_dir = state_dir; + restored.tool_manifest = manifest; + let (commands, receiver) = mpsc::channel(8); + let (events, mut received) = mpsc::channel(32); + let engine = tokio::spawn(run_engine( + restored, + receiver, + Arc::new(Notify::new()), + events, + )); + tokio::time::timeout(std::time::Duration::from_secs(3), async { + while let Some(event) = received.recv().await { + if event.kind() == "harness_idle" { + return; + } + } + panic!("restored engine did not start"); + }) + .await + .unwrap(); + commands.send(HarnessCommand::ToolResult { call_id:"cold-call".into(), result_json:r#"{"outcome":"failed","delivery":"available","output":{"content":[{"type":"text","text":"denied"}],"structuredContent":{"decision":"approve"},"isError":true},"resultUnavailable":false}"#.into() }).await.unwrap(); + let completed = tokio::time::timeout(std::time::Duration::from_secs(3), async { + while let Some(event) = received.recv().await { + if let HarnessEvent::ToolCallCompleted { + tool_call_id, ok, .. + } = event + { + if tool_call_id == "cold-call" { + return ok; + } + } + } + panic!("no result projection"); + }) + .await + .unwrap(); + assert!( + !completed, + "delivery must not turn an upstream failure into success" + ); + let requests = recorded_requests(&record).await; + assert!(requests + .iter() + .any(|request| request["method"] == "thread/resume")); + let text = requests + .iter() + .find(|request| request["method"] == "turn/start") + .unwrap()["params"]["input"][0]["text"] + .as_str() + .unwrap() + .to_string(); + assert!(text.contains(r#"{"structuredContent":{"decision":"approve"}}"#)); + assert!(text.contains(r#""isError":true"#)); + assert!( + !text.contains(r#""output":"#), + "present fields separately instead of an opaque envelope" + ); + assert_eq!( + engram_harness_sdk::mode_stamp::read_mode_stamp(&stamp), + "plan" + ); + engine.abort(); + } + #[tokio::test] async fn native_question_result_after_app_server_crash_becomes_follow_up_user_message() { let (script, record) = write_crashing_question_fake().await; diff --git a/crates/engram-harness-proto/src/lib.rs b/crates/engram-harness-proto/src/lib.rs index c3362134a..8e6349939 100644 --- a/crates/engram-harness-proto/src/lib.rs +++ b/crates/engram-harness-proto/src/lib.rs @@ -33,6 +33,7 @@ //! image small (one codec). pub mod attach_token; +pub mod tool_result; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -469,6 +470,14 @@ pub enum HarnessEvent { }, /// The harness re-attached while this run was in flight. RunContinued { run_id: String }, + /// Best-effort cancellation signal for the original call. The trusted + /// runtime must validate session, run and pending tool identity. This + /// event neither completes a journal entry nor reverses an effect. + ToolCallCancelRequested { + run_id: String, + call_id: String, + name: String, + }, } /// Who emitted an [`HarnessEvent::AgentMessage`]. @@ -501,6 +510,7 @@ impl HarnessEvent { | Self::AgentMessageChunk { run_id, .. } | Self::FileChanged { run_id, .. } | Self::ToolCallRequested { run_id, .. } + | Self::ToolCallCancelRequested { run_id, .. } | Self::BrowserActivity { run_id, .. } | Self::Generation { run_id, .. } | Self::RunCost { run_id, .. } @@ -533,6 +543,7 @@ impl HarnessEvent { Self::TitleSuggested { .. } => "title_suggested", Self::PromptSteered { .. } => "prompt_steered", Self::ToolCallRequested { .. } => "tool_call_requested", + Self::ToolCallCancelRequested { .. } => "tool_call_cancel_requested", Self::Idle => "harness_idle", Self::Parked => "harness_parked", Self::Busy => "harness_busy", @@ -551,7 +562,8 @@ impl HarnessEvent { | Self::ToolCallCompleted { tool_call_id, .. } | Self::FileChanged { tool_call_id, .. } | Self::BrowserActivity { tool_call_id, .. } => Some(tool_call_id.as_str()), - Self::ToolCallRequested { call_id, .. } => Some(call_id.as_str()), + Self::ToolCallRequested { call_id, .. } + | Self::ToolCallCancelRequested { call_id, .. } => Some(call_id.as_str()), _ => None, } } diff --git a/crates/engram-harness-proto/src/tool_result.rs b/crates/engram-harness-proto/src/tool_result.rs new file mode 100644 index 000000000..32b6f65ec --- /dev/null +++ b/crates/engram-harness-proto/src/tool_result.rs @@ -0,0 +1,267 @@ +//! Presentation of the trusted runtime's MCP completion envelope. +//! +//! The journal owns outcome and delivery. This module only validates and +//! presents those facts. It does not grant permission or prove an effect. + +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Value}; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ResultFormat { + #[default] + Json, + Mcp, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct McpToolResult { + #[serde(skip_serializing_if = "Option::is_none")] + source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + outcome: Option, + delivery: Delivery, + #[serde(skip_serializing_if = "Option::is_none")] + output: Option, + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + denial: Option, + #[serde(skip_serializing_if = "Option::is_none")] + result_unavailable: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum Source { + Runtime, + LocalValidation, +} +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum Outcome { + Succeeded, + Failed, + #[serde(rename = "outcome_unknown")] + Unknown, + Canceled, +} +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum Delivery { + Available, + Unavailable, +} +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum Reason { + UnsupportedContent, + ResultLimit, + OutputSchema, + UpstreamFailure, + ExecutionInterrupted, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct Output { + content: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + structured_content: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + is_error: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] +enum TextContent { + Text { text: String }, +} + +impl McpToolResult { + pub fn parse(raw: &str) -> Result { + // The normalized payload limit leaves room for the runtime receipt fields. + if raw.len() > 1024 * 1024 + 1024 { + return Err("External tool result exceeds the size limit."); + } + let document: Value = + serde_json::from_str(raw).map_err(|_| "Invalid external tool result.")?; + if document + .as_object() + .is_some_and(|object| object.values().any(Value::is_null)) + || document + .get("output") + .and_then(Value::as_object) + .is_some_and(|object| object.values().any(Value::is_null)) + { + return Err("Invalid external tool result."); + } + let result: Self = + serde_json::from_value(document).map_err(|_| "Invalid external tool result.")?; + let valid = match (result.delivery, result.output.as_ref()) { + (Delivery::Available, Some(output)) if result.reason.is_none() => { + match (result.source, result.outcome) { + (None, Some(Outcome::Succeeded)) => { + result.denial.is_none() && output.is_error != Some(true) + } + (None, Some(Outcome::Failed)) => { + result.denial.is_none() && output.is_error == Some(true) + } + (Some(Source::LocalValidation), None) => { + result.denial.is_none() && output.is_error == Some(true) + } + (Some(Source::Runtime), None) => { + result.denial.as_ref().is_some_and(|s| !s.is_empty()) + && output.is_error == Some(true) + } + _ => false, + } + } + (Delivery::Unavailable, None) => { + result.reason.is_some() + && result.denial.is_none() + && match (result.source, result.outcome) { + (None, Some(Outcome::Succeeded | Outcome::Failed | Outcome::Unknown)) => { + true + } + (Some(Source::Runtime), Some(Outcome::Canceled | Outcome::Unknown)) => { + result.reason == Some(Reason::ExecutionInterrupted) + } + _ => false, + } + } + _ => false, + }; + let unavailable_success = + result.outcome == Some(Outcome::Succeeded) && result.delivery == Delivery::Unavailable; + if !valid + || result + .result_unavailable + .is_some_and(|value| value != unavailable_success) + { + return Err("Invalid external tool result."); + } + Ok(result) + } + + pub fn success(&self) -> bool { + self.delivery == Delivery::Available && self.outcome == Some(Outcome::Succeeded) + } + + fn metadata(&self) -> Value { + let mut value = serde_json::to_value(self).expect("MCP result is JSON"); + let object = value.as_object_mut().expect("MCP result is an object"); + object.remove("output"); + if let Some(is_error) = self.output.as_ref().and_then(|output| output.is_error) { + object.insert("isError".into(), Value::Bool(is_error)); + } + value + } + + /// MCP has separate tool output and JSON-RPC delivery error surfaces. + pub fn claude_response(&self, id: Value) -> Value { + match &self.output { + Some(output) => { + let mut result = serde_json::to_value(output).expect("MCP output is JSON"); + result["_meta"] = json!({"engrams/toolResult": self.metadata()}); + json!({"jsonrpc":"2.0", "id":id, "result":result}) + } + None => json!({"jsonrpc":"2.0", "id":id, "error":{ + "code":-32603, "message":"External tool output is unavailable.", "data":self.metadata() + }}), + } + } + + /// Codex supports text items and one success flag. Keep each original text + /// item and present the other named fields as separate JSON text items. + pub fn text_items(&self) -> Vec { + let mut items = Vec::new(); + if let Some(output) = &self.output { + for TextContent::Text { text } in &output.content { + items.push(text.clone()); + } + if let Some(structured) = &output.structured_content { + items.push(json!({"structuredContent":structured}).to_string()); + } + } + items.push(self.metadata().to_string()); + items + } + + pub fn codex_response(&self) -> Value { + let items: Vec = self + .text_items() + .into_iter() + .map(|text| json!({"type":"inputText","text":text})) + .collect(); + json!({"success":self.success(), "contentItems":items}) + } +} + +pub fn invalid_codex_result() -> Value { + json!({"success":false,"contentItems":[{"type":"inputText", "text": + json!({"delivery":"unavailable","reason":"invalid_result"}).to_string()}]}) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_binary_resources_and_null_optional_fields() { + for output in [ + json!({"content":[{"type":"image","data":"AA==","mimeType":"image/png"}]}), + json!({"content":[{"type":"resource_link","uri":"https://example.test/x"}]}), + json!({"content":[{"type":"resource","resource":{"uri":"https://example.test/x","text":"x"}}]}), + json!({"content":[],"structuredContent":null}), + json!({"content":[],"structuredContent":[]}), + json!({"content":[],"isError":null}), + ] { + let envelope = json!({"outcome":"succeeded","delivery":"available","output":output}); + assert!( + McpToolResult::parse(&envelope.to_string()).is_err(), + "accepted {envelope}" + ); + } + } + + #[test] + fn delivery_error_keeps_outcome_and_never_becomes_tool_error() { + let result = McpToolResult::parse(r#"{"outcome":"succeeded","delivery":"unavailable","reason":"unsupported_content","resultUnavailable":true}"#).unwrap(); + let response = result.claude_response(json!(7)); + assert!(response.get("result").is_none()); + assert_eq!( + response["error"]["data"], + json!({"outcome":"succeeded","delivery":"unavailable","reason":"unsupported_content","resultUnavailable":true}) + ); + assert!(!result.success()); + } + + #[test] + fn local_denials_and_runtime_interruptions_do_not_claim_upstream_failure() { + for raw in [ + r#"{"source":"local_validation","delivery":"available","output":{"content":[{"type":"text","text":"invalid"}],"isError":true},"resultUnavailable":false}"#, + r#"{"source":"runtime","denial":"plan_mode","delivery":"available","output":{"content":[{"type":"text","text":"denied"}],"isError":true}}"#, + r#"{"source":"runtime","outcome":"canceled","delivery":"unavailable","reason":"execution_interrupted","resultUnavailable":false}"#, + ] { + let result = McpToolResult::parse(raw).unwrap(); + assert!(!result.success()); + let serialized = serde_json::to_value(&result).unwrap(); + assert_ne!(serialized["outcome"], "failed"); + } + } + + #[test] + fn rejects_inconsistent_outcome_and_delivery() { + for raw in [ + r#"{"outcome":"succeeded","delivery":"available","output":{"content":[],"isError":true}}"#, + r#"{"outcome":"failed","delivery":"available","output":{"content":[]}}"#, + r#"{"outcome":"outcome_unknown","delivery":"available","output":{"content":[]}}"#, + r#"{"outcome":"succeeded","delivery":"unavailable","reason":"result_limit","resultUnavailable":false}"#, + r#"{"outcome":"succeeded","delivery":"available","output":{"content":[]},"decision":"approve"}"#, + ] { + assert!(McpToolResult::parse(raw).is_err(), "accepted {raw}"); + } + } +} diff --git a/crates/engram-harness-proto/tests/decode_never_panics.rs b/crates/engram-harness-proto/tests/decode_never_panics.rs index d20cf0efd..67bf137b4 100644 --- a/crates/engram-harness-proto/tests/decode_never_panics.rs +++ b/crates/engram-harness-proto/tests/decode_never_panics.rs @@ -41,6 +41,9 @@ use proptest::prelude::*; /// them *panic*. A panic here unwinds out of the closure and fails the /// proptest case with the shrunk counterexample. fn decode_every_type(bytes: &[u8]) { + if let Ok(raw) = std::str::from_utf8(bytes) { + let _ = engram_harness_proto::tool_result::McpToolResult::parse(raw); + } let _ = serde_json::from_slice::(bytes); let _ = bincode::deserialize::(bytes); let _ = bincode::deserialize::(bytes); diff --git a/crates/engram-harness-proto/tests/golden/event_tool_call_cancel_requested.bin b/crates/engram-harness-proto/tests/golden/event_tool_call_cancel_requested.bin new file mode 100644 index 0000000000000000000000000000000000000000..2e5714f7cb2550e849ecfdca0abf690730d4a876 GIT binary patch literal 55 wcmWe-U|?VcVh|`Q&C@kxgRmKr6LWHO4F#aQqSV~{lGOOjyt351lKi4d0KYN{w*UYD literal 0 HcmV?d00001 diff --git a/crates/engram-harness-proto/tests/support/mod.rs b/crates/engram-harness-proto/tests/support/mod.rs index 1ee800910..8a44614e8 100644 --- a/crates/engram-harness-proto/tests/support/mod.rs +++ b/crates/engram-harness-proto/tests/support/mod.rs @@ -66,6 +66,13 @@ pub fn file_change() -> impl Strategy { pub fn harness_event() -> impl Strategy { prop_oneof![ + (s(), s(), s()).prop_map( + |(run_id, call_id, name)| HarnessEvent::ToolCallCancelRequested { + run_id, + call_id, + name + } + ), (s(), opt_s(), opt_s()).prop_map(|(run_id, prompt_summary, prompt_id)| { HarnessEvent::RunStarted { run_id, @@ -389,6 +396,7 @@ fn _exhaustiveness_harness_event(e: &HarnessEvent) { HarnessEvent::TitleSuggested { .. } => {} HarnessEvent::PromptSteered { .. } => {} HarnessEvent::ToolCallRequested { .. } => {} + HarnessEvent::ToolCallCancelRequested { .. } => {} HarnessEvent::Parked => {} HarnessEvent::BrowserActivity { .. } => {} HarnessEvent::Busy => {} diff --git a/crates/engram-harness-proto/tests/wire_golden.rs b/crates/engram-harness-proto/tests/wire_golden.rs index 8db1a5b38..27738abf9 100644 --- a/crates/engram-harness-proto/tests/wire_golden.rs +++ b/crates/engram-harness-proto/tests/wire_golden.rs @@ -322,6 +322,14 @@ fn cmd_tool_result() -> HarnessCommand { } } +fn ev_tool_call_cancel_requested() -> HarnessEvent { + HarnessEvent::ToolCallCancelRequested { + run_id: "run-1".into(), + call_id: "call-1".into(), + name: "remote_inventory".into(), + } +} + // ---- tests ------------------------------------------------------------- #[test] @@ -348,6 +356,15 @@ fn harness_frame_golden_and_variant_indices() { #[test] fn harness_event_golden_and_variant_indices() { + assert_golden( + "event_tool_call_cancel_requested", + &ev_tool_call_cancel_requested(), + ); + assert_variant_index( + &ev_tool_call_cancel_requested(), + 21, + "HarnessEvent::ToolCallCancelRequested", + ); assert_golden("event_run_continued", &ev_run_continued()); assert_variant_index(&ev_run_continued(), 20, "HarnessEvent::RunContinued"); assert_golden("event_run_started", &ev_run_started()); @@ -633,6 +650,10 @@ fn regen_golden() { ); write("frame_event_ack", &HarnessFrame::EventAck { seq: 42 }); + write( + "event_tool_call_cancel_requested", + &ev_tool_call_cancel_requested(), + ); write("event_run_continued", &ev_run_continued()); write("event_run_started", &ev_run_started()); write("event_agent_message", &ev_agent_message()); diff --git a/crates/engram-harness-sdk/src/first_output.rs b/crates/engram-harness-sdk/src/first_output.rs index cb12f1497..cfed53fc7 100644 --- a/crates/engram-harness-sdk/src/first_output.rs +++ b/crates/engram-harness-sdk/src/first_output.rs @@ -107,7 +107,8 @@ fn output_run_id(event: &HarnessEvent) -> Option<&str> { | HarnessEvent::Parked | HarnessEvent::BrowserActivity { .. } | HarnessEvent::Busy - | HarnessEvent::RunCost { .. } => None, + | HarnessEvent::RunCost { .. } + | HarnessEvent::ToolCallCancelRequested { .. } => None, } } diff --git a/crates/engram-protocol/src/wire.rs b/crates/engram-protocol/src/wire.rs index c8c2e6479..45823399e 100644 --- a/crates/engram-protocol/src/wire.rs +++ b/crates/engram-protocol/src/wire.rs @@ -171,7 +171,8 @@ use serde::{Deserialize, Serialize}; // the Dead budget instead of recovering from disk. // v36 gates page-channel v6 fault windows for a lockstep host roll. // v37 appends HostCapabilities.page_idle and the optional MigrationItem::HotHint. -pub const WIRE_VERSION: u32 = 38; +// v39 appends the harness per-call cancellation signal. Older fleets cannot decode it. +pub const WIRE_VERSION: u32 = 39; /// gRPC metadata (header) key carrying the caller's [`WIRE_VERSION`] on /// every coord→host request (issue #229). ASCII, lowercase — tonic diff --git a/crates/engram-protocol/tests/wire_golden.rs b/crates/engram-protocol/tests/wire_golden.rs index c078d5d33..c8aee28f8 100644 --- a/crates/engram-protocol/tests/wire_golden.rs +++ b/crates/engram-protocol/tests/wire_golden.rs @@ -736,7 +736,7 @@ fn wire_version_pinned() { // Existing goldens embed neither payload and keep their bytes. // 37 -> 38: SessionEgressPolicy appends the managed deny envelope. assert_eq!( - WIRE_VERSION, 38, + WIRE_VERSION, 39, "WIRE_VERSION changed — confirm payload goldens were regenerated too" ); } diff --git a/orchestrator/src/__tests__/mcp-session-bindings.test.ts b/orchestrator/src/__tests__/mcp-session-bindings.test.ts index 88a3ebdcd..475a27be0 100644 --- a/orchestrator/src/__tests__/mcp-session-bindings.test.ts +++ b/orchestrator/src/__tests__/mcp-session-bindings.test.ts @@ -21,6 +21,7 @@ test("session bindings preserve reviewed schema and freeze every authority input const manifest = compileMcpManifest(snapshot); expect(manifest[0]?.inputSchema).toEqual(schema); expect(manifest[0]?.execution).toBe("sync"); + expect(manifest[0]?.resultFormat).toBe("mcp"); expect(snapshot.managedDenyPolicy.hosts).toEqual(["alias.example.test", "tools.example.test"]); const original = structuredClone(snapshot); authority.connections[0]!.tools[0]!.inputSchema = { type: "null" }; @@ -123,6 +124,7 @@ test("write descriptors stay deferred and catalog changes cannot mutate old mani const snapshot = compile(authority); const manifest = compileMcpManifest(snapshot); expect(manifest[0]!.execution).toBe("deferred"); + expect(manifest[0]!.resultFormat).toBe("mcp"); manifest[0]!.inputSchema = {}; expect(compileMcpManifest(snapshot)[0]!.inputSchema).toEqual(schema); }); diff --git a/orchestrator/src/mcp/session-bindings.ts b/orchestrator/src/mcp/session-bindings.ts index df1005d8d..735b23c06 100644 --- a/orchestrator/src/mcp/session-bindings.ts +++ b/orchestrator/src/mcp/session-bindings.ts @@ -336,6 +336,7 @@ export function compileMcpManifest(snapshot: McpSessionBindingSnapshot): ToolMan inputSchema: structuredClone(bound.tool.inputSchema), execution: bound.tool.classification === "write" ? ("deferred" as const) : ("sync" as const), nativeBindings: {}, + resultFormat: "mcp" as const, })), ); if ( diff --git a/orchestrator/src/tools/manifest.ts b/orchestrator/src/tools/manifest.ts index 5c3617313..795ce3acb 100644 --- a/orchestrator/src/tools/manifest.ts +++ b/orchestrator/src/tools/manifest.ts @@ -14,6 +14,7 @@ export interface ToolManifestEntry { inputSchema: object; execution: "sync" | "deferred"; nativeBindings: NativeBindings; + resultFormat?: "mcp"; } /** Compile the model-facing manifest from a registry without side effects. */ From bc64c220e2e2d9e869d27fedc4c7d788bc2dbef0 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 11:59:00 -0500 Subject: [PATCH 034/279] Protect replacement MCP calls from stale socket cleanup Bind each pending Claude bridge socket to its own ownership token. Disconnect cleanup clears the sender and requests cancellation only while that socket still owns the stable call ID. Add a deterministic two-socket regression that delays the old cleanup until a replacement is registered, then checks result delivery and the absence of stale cancellation. --- crates/engram-harness-claude/src/main.rs | 132 +++++++++++++++++++++-- 1 file changed, 125 insertions(+), 7 deletions(-) diff --git a/crates/engram-harness-claude/src/main.rs b/crates/engram-harness-claude/src/main.rs index c8f6a9fd0..225d2ccb1 100644 --- a/crates/engram-harness-claude/src/main.rs +++ b/crates/engram-harness-claude/src/main.rs @@ -1571,6 +1571,7 @@ mod adapter { pub type ParkedCalls = Arc>>; pub struct PendingCall { + socket_owner: Arc<()>, pub tool_name: String, pub result_format: ResultFormat, pub sender: Option>, @@ -1718,6 +1719,7 @@ mod adapter { .filter(|id| !id.is_empty()) .unwrap_or_else(|| format!("toolu-engram-{}", uuid::Uuid::new_v4())); + let socket_owner = Arc::new(()); // Both this insertion and `route_tool_result` lock parked→results. // Holding the parked lock across the stash check closes the only // lost-wakeup window: a result can neither slip between the check @@ -1761,6 +1763,7 @@ mod adapter { parked.insert( call_id.clone(), PendingCall { + socket_owner: socket_owner.clone(), tool_name: request.name.clone(), result_format: manifest .iter() @@ -1791,17 +1794,43 @@ mod adapter { Err(_) => tracing::debug!(%call_id, "parked MCP call was canceled"), }, _ = lines.next_line() => { - // Keep identity after disconnect for late host results. - if let Some(call) = parked_calls.lock().await.get_mut(&call_id) { - call.sender.take(); - } - emit(&evt_tx, HarnessEvent::ToolCallCancelRequested { - run_id, call_id, name: request.name, - }).await; + cancel_disconnected_call(&parked_calls, &socket_owner, &evt_tx, + run_id, call_id, request.name).await; } } } + async fn cancel_disconnected_call( + parked_calls: &ParkedCalls, + socket_owner: &Arc<()>, + evt_tx: &mpsc::Sender, + run_id: String, + call_id: String, + name: String, + ) { + let canceled = { + let mut calls = parked_calls.lock().await; + calls + .get_mut(&call_id) + .filter(|call| Arc::ptr_eq(&call.socket_owner, socket_owner)) + // Keep identity after disconnect for late host results. + .is_some_and(|call| call.sender.take().is_some()) + }; + // A re-fire can replace this ID while the old socket waits for + // the map lock. Only the socket that still owns it may cancel. + if canceled { + emit( + evt_tx, + HarnessEvent::ToolCallCancelRequested { + run_id, + call_id, + name, + }, + ) + .await; + } + } + async fn write_result( writer: &mut W, result_json: String, @@ -1846,6 +1875,95 @@ mod adapter { .insert(call_id.to_string(), result_json); false } + #[cfg(test)] + mod socket_ownership_tests { + use super::*; + + #[tokio::test] + async fn stale_socket_disconnect_cannot_cancel_replacement_call() { + let (evt_tx, mut events) = mpsc::channel(8); + let state = State { + results_in_hand: Arc::new(Mutex::new(HashMap::new())), + parked_calls: Arc::new(Mutex::new(HashMap::new())), + manifest: Arc::new(super::super::parse_tool_manifest(r#"[{"name":"remote_inventory","description":"Read","inputSchema":{"type":"object"},"execution":"sync","nativeBindings":{},"resultFormat":"mcp"}]"#).unwrap()), + deferred_calls: Arc::new(Mutex::new(HashMap::new())), + current_run_id: Arc::new(Mutex::new(Some("run-1".into()))), + evt_tx, + }; + let request = + b"{\"name\":\"remote_inventory\",\"tool_use_id\":\"same-call\",\"args\":{}}\n"; + let (socket_a, mut client_a) = UnixStream::pair().unwrap(); + client_a.write_all(request).await.unwrap(); + let mut a = std::pin::pin!(handle_one(socket_a, state.clone())); + tokio::select! { + () = &mut a => panic!("socket A must remain pending"), + event = events.recv() => assert!(matches!(event, Some(HarnessEvent::ToolCallRequested { .. }))), + } + let owner_a = state.parked_calls.lock().await["same-call"] + .socket_owner + .clone(); + + // Pause A before its EOF cleanup. B uses the same stable ID, + // then the exact production cleanup step for A runs late. + drop(client_a); + let (socket_b, mut client_b) = UnixStream::pair().unwrap(); + client_b.write_all(request).await.unwrap(); + let mut b = std::pin::pin!(handle_one(socket_b, state.clone())); + tokio::select! { + () = &mut b => panic!("socket B must remain pending"), + event = events.recv() => assert!(matches!(event, Some(HarnessEvent::ToolCallRequested { .. }))), + } + { + let calls = state.parked_calls.lock().await; + let replacement = &calls["same-call"]; + assert!(!Arc::ptr_eq(&replacement.socket_owner, &owner_a)); + assert_eq!(replacement.tool_name, "remote_inventory"); + assert_eq!(replacement.result_format, ResultFormat::Mcp); + } + cancel_disconnected_call( + &state.parked_calls, + &owner_a, + &state.evt_tx, + "run-1".into(), + "same-call".into(), + "remote_inventory".into(), + ) + .await; + tokio::time::timeout(std::time::Duration::from_secs(1), &mut a) + .await + .unwrap(); + let result = r#"{"outcome":"succeeded","delivery":"available","output":{"content":[{"type":"text","text":"replacement result"}]}}"#; + assert!( + route_tool_result( + "same-call", + result.into(), + &state.parked_calls, + &state.results_in_hand + ) + .await, + "the replacement socket must still accept its result" + ); + tokio::time::timeout(std::time::Duration::from_secs(1), &mut b) + .await + .unwrap(); + let response = BufReader::new(client_b) + .lines() + .next_line() + .await + .unwrap() + .unwrap(); + assert_eq!( + serde_json::from_str::(&response).unwrap()["result_json"], + result + ); + assert!( + matches!(events.try_recv(), Err(mpsc::error::TryRecvError::Empty)), + "stale cleanup must not emit cancellation for B" + ); + assert!(state.parked_calls.lock().await.is_empty()); + assert!(state.results_in_hand.lock().await.is_empty()); + } + } } /// Aborts the hook-socket accept task and unlinks the socket file when From 02c3aef09e6c79683d15797dff48d609ab71b7c3 Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Mon, 2 Nov 2026 11:19:21 -0500 Subject: [PATCH 035/279] Add external MCP administration and grant review --- .../proto/engram/app/v1/integration.proto | 11 + .../src/__tests__/mcp-catalog-read.test.ts | 25 + orchestrator/src/__tests__/mcp-rpc.test.ts | 100 ++- .../src/gen/engram/app/v1/integration_pb.ts | 154 +++-- orchestrator/src/mcp/contracts.ts | 45 +- orchestrator/src/mcp/credential-rpc.ts | 1 + orchestrator/src/mcp/repository.ts | 11 +- web/index.html | 1 + web/src/App.tsx | 3 +- web/src/app-entry.tsx | 28 + .../integrations/GoogleCloudSetupPage.tsx | 13 +- .../integrations/IntegrationDetail.tsx | 8 + .../integrations/McpCatalogReview.test.tsx | 95 +++ .../integrations/McpCatalogReview.tsx | 217 +++++++ .../integrations/McpConnections.test.tsx | 175 +++++ .../integrations/McpConnections.tsx | 613 ++++++++++++++++++ .../profiles/McpGrantEditor.test.tsx | 107 +++ .../components/profiles/McpGrantEditor.tsx | 266 ++++++++ .../settings/IntegrationsPanel.test.tsx | 7 + .../components/settings/IntegrationsPanel.tsx | 19 +- ...gration-IntegrationService_connectquery.ts | 5 + web/src/gen/engram/app/v1/integration_pb.ts | 154 +++-- web/src/index-html.test.ts | 11 + web/src/main.test.ts | 31 + web/src/main.tsx | 34 +- web/src/mcp-callback-bootstrap.test.ts | 83 +++ web/src/mcp-callback-bootstrap.ts | 114 ++++ web/src/mcp-callback.test.ts | 68 ++ web/src/mcp-callback.ts | 45 ++ .../settings/SessionProfileEditor.test.tsx | 72 ++ .../pages/settings/SessionProfileEditor.tsx | 173 ++++- 31 files changed, 2550 insertions(+), 139 deletions(-) create mode 100644 orchestrator/src/__tests__/mcp-catalog-read.test.ts create mode 100644 web/src/app-entry.tsx create mode 100644 web/src/components/integrations/McpCatalogReview.test.tsx create mode 100644 web/src/components/integrations/McpCatalogReview.tsx create mode 100644 web/src/components/integrations/McpConnections.test.tsx create mode 100644 web/src/components/integrations/McpConnections.tsx create mode 100644 web/src/components/profiles/McpGrantEditor.test.tsx create mode 100644 web/src/components/profiles/McpGrantEditor.tsx create mode 100644 web/src/index-html.test.ts create mode 100644 web/src/main.test.ts create mode 100644 web/src/mcp-callback-bootstrap.test.ts create mode 100644 web/src/mcp-callback-bootstrap.ts create mode 100644 web/src/mcp-callback.test.ts create mode 100644 web/src/mcp-callback.ts diff --git a/crates/engram-protocol/proto/engram/app/v1/integration.proto b/crates/engram-protocol/proto/engram/app/v1/integration.proto index f4c0b5edf..c0e86999a 100644 --- a/crates/engram-protocol/proto/engram/app/v1/integration.proto +++ b/crates/engram-protocol/proto/engram/app/v1/integration.proto @@ -54,6 +54,7 @@ service IntegrationService { rpc TestConnection(TestConnectionRequest) returns (TestConnectionResponse); rpc SetConnectionEnabled(SetConnectionEnabledRequest) returns (SetConnectionEnabledResponse); rpc GetGoogleCloudSetup(GetGoogleCloudSetupRequest) returns (GetGoogleCloudSetupResponse); + rpc GetMcpCatalogs(GetMcpCatalogsRequest) returns (GetMcpCatalogsResponse); rpc DiscoverMcp(DiscoverMcpRequest) returns (DiscoverMcpResponse); rpc PublishMcp(PublishMcpRequest) returns (PublishMcpResponse); rpc PutMcpCredential(PutMcpCredentialRequest) returns (PutMcpCredentialResponse); @@ -373,6 +374,15 @@ message McpCatalog { repeated McpTool tools = 7; string fingerprint = 8; } +message GetMcpCatalogsRequest { + string connection_id = 1; +} +message GetMcpCatalogsResponse { + uint32 config_revision = 1; + uint32 catalog_revision = 2; + McpCatalog draft = 3; + McpCatalog published = 4; +} message DiscoverMcpRequest { string connection_id = 1; uint32 expected_revision = 2; @@ -560,6 +570,7 @@ message GetMcpSetupResponse { string flow_id = 6; string flow_expires_at = 7; bool client_secret_configured = 8; + bool discovery_available = 9; } message CompleteMcpAuthorizationRequest { string connection_id = 1; diff --git a/orchestrator/src/__tests__/mcp-catalog-read.test.ts b/orchestrator/src/__tests__/mcp-catalog-read.test.ts new file mode 100644 index 000000000..7cddf6008 --- /dev/null +++ b/orchestrator/src/__tests__/mcp-catalog-read.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "bun:test"; +import { reviewFixture } from "./mcp-review-fixture.ts"; + +test("reads only the latest persisted draft and selected published revision", async () => { + const f = await reviewFixture(); + try { + const current = (await f.connections.get(f.connection.id))!; + const newer = await f.repository.saveDraft( + f.connection.id, + current.mcp!, + [{ upstreamName: "Read.New", description: "New", inputSchema: { type: "object" } }], + f.actorId, + new Date(f.now.getTime() + 1000), + ); + const draft = await f.repository.getLatestDraft(f.connection.id); + const published = await f.repository.getCatalog(f.connection.id, current.mcp!.catalogRevision); + expect(draft?.id).toBe(newer.id); + expect(draft?.tools[0]?.upstreamName).toBe("Read.New"); + expect(published?.id).toBe(f.catalog.id); + expect(published?.revision).toBe(1); + expect(await f.repository.getCatalog(f.connection.id, 2)).toBeNull(); + } finally { + await f.cleanup(); + } +}, 20000); diff --git a/orchestrator/src/__tests__/mcp-rpc.test.ts b/orchestrator/src/__tests__/mcp-rpc.test.ts index ffccc5e1f..bf1d5164c 100644 --- a/orchestrator/src/__tests__/mcp-rpc.test.ts +++ b/orchestrator/src/__tests__/mcp-rpc.test.ts @@ -62,7 +62,7 @@ function services(): McpServices { saveDraft: unavailable, publish: unavailable, getCatalog: unavailable, - listCatalogs: unavailable, + getLatestDraft: unavailable, }, journal: { get: unavailable, @@ -89,13 +89,16 @@ function services(): McpServices { }, }; } -async function spawn(service?: McpServices, role = "admin", email?: string) { +async function spawn(service?: McpServices, role = "admin", email?: string, connection = row) { const server = buildServer(new Hono(), (router) => router.service( IntegrationService, mcpHandlers({ services: service, - connections, + connections: { + ...connections, + get: async (id) => (id === connection.id ? connection : null), + }, getSession: async () => ({ user: { id: "owner", role, email } }), now: () => new Date(0), connectionToProto: (c) => ({ id: c.id }), @@ -165,6 +168,97 @@ describe("MCP native wire contract", () => { await admin.close(); } }); + test("setup reports only a registered enabled discovery hook", async () => { + const service = services(); + const admin = await spawn(service); + try { + expect((await admin.client.getMcpSetup({})).discoveryAvailable).toBe(false); + service.discovery = { discover: unavailable }; + expect((await admin.client.getMcpSetup({})).discoveryAvailable).toBe(true); + service.enabled = false; + expect((await admin.client.getMcpSetup({})).discoveryAvailable).toBe(false); + } finally { + await admin.close(); + } + }); + test("catalog read requires an admin and returns the persisted draft and current publication", async () => { + const service = services(); + const base = { + connectionId: row.id, + configRevision: 1, + authGeneration: 1, + credentialIdentity: "account-a", + actorId: "owner", + createdAt: new Date(0), + fingerprint: "hash", + }; + service.repository.getLatestDraft = async () => ({ + ...base, + id: "draft", + kind: "draft", + revision: 0, + tools: [ + { + upstreamName: "read.item", + description: "", + inputSchema: { type: "object" }, + }, + ], + }); + service.repository.getCatalog = async (_id, revision) => ({ + ...base, + id: "published", + kind: "published", + revision, + tools: [ + { + upstreamName: "read.item", + description: "old", + inputSchema: { type: "object" }, + classification: "read", + enabled: true, + fingerprint: "tool", + }, + ], + }); + const current = { ...row, mcp: { ...row.mcp!, catalogRevision: 2 } }; + const admin = await spawn(service, "admin", undefined, current); + const member = await spawn(service, "user"); + const serviceAccount = await spawn(service, "admin", "apikey+agent@service.local"); + try { + await code( + serviceAccount.client.getMcpCatalogs({ connectionId: row.id }), + Code.PermissionDenied, + ); + await code(member.client.getMcpCatalogs({ connectionId: row.id }), Code.PermissionDenied); + const result = await admin.client.getMcpCatalogs({ connectionId: row.id }); + expect(result.draft?.tools[0]?.classification).toBe(""); + expect(result.draft?.tools[0]?.description).toBe(""); + expect(result.published?.revision).toBe(2); + expect(result.published?.tools[0]?.classification).toBe("read"); + expect(result.catalogRevision).toBe(2); + service.enabled = false; + expect( + (await admin.client.getMcpCatalogs({ connectionId: row.id })).published?.revision, + ).toBe(2); + service.repository.getLatestDraft = async () => ({ + ...base, + id: "oversized", + kind: "draft", + revision: 0, + tools: Array.from({ length: 129 }, (_, i) => ({ + upstreamName: `tool-${i}`, + description: "", + inputSchema: { type: "object" }, + })), + }); + await code(admin.client.getMcpCatalogs({ connectionId: row.id }), Code.FailedPrecondition); + } finally { + await member.close(); + await serviceAccount.close(); + await admin.close(); + } + }); test("publication preserves schema and classification and maps stale CAS", async () => { const service = services(); let calls = 0; diff --git a/orchestrator/src/gen/engram/app/v1/integration_pb.ts b/orchestrator/src/gen/engram/app/v1/integration_pb.ts index 3b03a81cb..fa0373a33 100644 --- a/orchestrator/src/gen/engram/app/v1/integration_pb.ts +++ b/orchestrator/src/gen/engram/app/v1/integration_pb.ts @@ -12,7 +12,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file engram/app/v1/integration.proto. */ export const file_engram_app_v1_integration: GenFile = /*@__PURE__*/ - fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSJGChJEaXNjb3Zlck1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDSJBChNEaXNjb3Zlck1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cifgoRUHVibGlzaE1jcFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIQCghkcmFmdF9pZBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIlCgV0b29scxgEIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbCJAChJQdWJsaXNoTWNwUmVzcG9uc2USKgoHY2F0YWxvZxgBIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZyKFAQoTQXV0aG9yaXplTWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgEIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0SHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYAyABKAgiiAEKFEF1dGhvcml6ZU1jcFJlc3BvbnNlEhcKD2NvbmZpZ19yZXZpc2lvbhgEIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YBSABKA0SDwoHZmxvd19pZBgCIAEoCRISCgpleHBpcmVzX2F0GAMgASgJEhkKEWF1dGhvcml6YXRpb25fdXJsGAEgASgJImUKFERpc2Nvbm5lY3RNY3BSZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAMgASgNEhUKDWNvbm5lY3Rpb25faWQYASABKAkSGwoTZXhwZWN0ZWRfZ2VuZXJhdGlvbhgCIAEoDSJRChVEaXNjb25uZWN0TWNwUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIo8DCgtNY3BBcHByb3ZhbBIUCgxleGVjdXRpb25faWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIOCgZydW5faWQYAyABKAkSFAoMdG9vbF9jYWxsX2lkGAQgASgJEhUKDWNvbm5lY3Rpb25faWQYBSABKAkSFQoNdXBzdHJlYW1fbmFtZRgGIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgHIAEoCRIVCg1hcmd1bWVudF9oYXNoGAggASgJEhIKCmV4cGlyZXNfYXQYCSABKAkSGgoSb3JpZ2luYWxfYXJndW1lbnRzGAogASgJEi0KDXJldmlld2VkX3Rvb2wYCyABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgMIAMoCRJIChdleHBlY3RlZF9yZXZpZXdfY29udGV4dBgNIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IkkKF0xpc3RNY3BBcHByb3ZhbHNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSGgoSYWZ0ZXJfZXhlY3V0aW9uX2lkGAIgASgJImQKGExpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRItCglhcHByb3ZhbHMYASADKAsyGi5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsEhkKEW5leHRfZXhlY3V0aW9uX2lkGAIgASgJIosBChhEZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEg8KB2FwcHJvdmUYAiABKAgSSAoXZXhwZWN0ZWRfcmV2aWV3X2NvbnRleHQYAyABKAsyJy5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dCJeChlEZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJEjIKB3JlY2VpcHQYAiABKAsyIS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmVjZWlwdCJPChRDYW5jZWxNY3BDYWxsUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSIQoZZXhwZWN0ZWRfam91cm5hbF9yZXZpc2lvbhgCIAEoDSJeChVDYW5jZWxNY3BDYWxsUmVzcG9uc2USDQoFc3RhdGUYASABKAkSNgoHcmVjZWlwdBgCIAEoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQ2FuY2VsbGF0aW9uUmVjZWlwdCKFAQoYUmVzb2x2ZU1jcE91dGNvbWVSZXF1ZXN0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIPCgdvdXRjb21lGAIgASgJEkIKGGV4cGVjdGVkX3Vua25vd25fY29udGV4dBgDIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwVW5rbm93bkNvbnRleHQiYgoZUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRIPCgdvdXRjb21lGAEgASgJEjQKB3JlY2VpcHQYAiABKAsyIy5lbmdyYW0uYXBwLnYxLk1jcFJlc29sdXRpb25SZWNlaXB0IkwKF1ByZXBhcmVNY3BSZWJpbmRSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSHQoVZXhwZWN0ZWRfYmluZGluZ19oYXNoGAIgASgJIqkCChhQcmVwYXJlTWNwUmViaW5kUmVzcG9uc2USFAoMYmluZGluZ19oYXNoGAEgASgJEhUKDWJpbmRpbmdfZXBvY2gYAiABKAkSOQoNYmluZGluZ190b29scxgDIAMoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQm91bmRUb29sU3VtbWFyeRISCgpkZW55X2hvc3RzGAQgAygJEhUKDWRlbnlfcmV2aXNpb24YBSABKAkSEwoLZGVueV9kaWdlc3QYBiABKAkSNwoLY29ubmVjdGlvbnMYByADKAsyIi5lbmdyYW0uYXBwLnYxLk1jcFJlYmluZENvbm5lY3Rpb24SLAoFZHJhaW4YCCABKAsyHS5lbmdyYW0uYXBwLnYxLk1jcFJlYmluZERyYWluIloKDk1jcFJlYmluZERyYWluEjMKBnJlYXNvbhgBIAEoDjIjLmVuZ3JhbS5hcHAudjEuTWNwUmViaW5kRHJhaW5SZWFzb24SEwoLcmV0cnlfYWZ0ZXIYAiABKAkixAEKE01jcFJlYmluZENvbm5lY3Rpb24SFQoNY29ubmVjdGlvbl9pZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRIMCgR6b25lGAQgASgJEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYBSABKAkSFwoPY29uZmlnX3JldmlzaW9uGAYgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgHIAEoDRIYChBjYXRhbG9nX3JldmlzaW9uGAggASgNImYKEFJlYmluZE1jcFJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIdChVleHBlY3RlZF9iaW5kaW5nX2hhc2gYAiABKAkSHwoXZXhwZWN0ZWRfY2FuZGlkYXRlX2hhc2gYAyABKAkiKQoRUmViaW5kTWNwUmVzcG9uc2USFAoMYmluZGluZ19oYXNoGAEgASgJIpUBChdQdXRNY3BDcmVkZW50aWFsUmVxdWVzdBIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SDAoEa2luZBgEIAEoCRIOCgZoZWFkZXIYBSABKAkSDQoFdmFsdWUYBiABKAkilAEKE01jcENyZWRlbnRpYWxTdGF0dXMSDgoGc3RhdHVzGAEgASgJEhEKCXJlZmVyZW5jZRgCIAEoCRIMCgRraW5kGAMgASgJEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYBCABKAkSDwoHdmVyc2lvbhgFIAEoAxIeChZyZXNlcnZhdGlvbl9nZW5lcmF0aW9uGAYgASgDIowBChhQdXRNY3BDcmVkZW50aWFsUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMilAEKIENvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbhI2CgpjcmVkZW50aWFsGAIgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdHVzIpABChxSZWNvdmVyTWNwQ3JlZGVudGlhbFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbhI2CgpjcmVkZW50aWFsGAIgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdHVzIoYBChJNY3BDcmVkZW50aWFsU3RhdGUSOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uEjYKCmNyZWRlbnRpYWwYAiABKAsyIi5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0dXMiOwoWTWNwQXBwcm92ZWREZXN0aW5hdGlvbhIPCgd6b25lX2lkGAEgASgJEhAKCGVuZHBvaW50GAIgASgJIrsBChVNY3BDbGllbnRSZWdpc3RyYXRpb24SCgoCaWQYASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhQKDGNvbmZpZGVudGlhbBgDIAEoCBIUCgxyZWRpcmVjdF91cmkYBCABKAkSDgoGaXNzdWVyGAUgASgJEjcKCHJlc291cmNlGAYgASgLMiUuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZlZERlc3RpbmF0aW9uEg4KBnNjb3BlcxgHIAMoCSIrChJHZXRNY3BTZXR1cFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCSK8AgoTR2V0TWNwU2V0dXBSZXNwb25zZRIPCgdlbmFibGVkGAEgASgIEjsKDXJlZ2lzdHJhdGlvbnMYAiADKAsyJC5lbmdyYW0uYXBwLnYxLk1jcENsaWVudFJlZ2lzdHJhdGlvbhI7CgxkZXN0aW5hdGlvbnMYAyADKAsyJS5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmVkRGVzdGluYXRpb24SMgoHY3VycmVudBgEIAEoCzIhLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXRlEhoKEnRyYW5zaXRpb25fcGVuZGluZxgFIAEoCBIPCgdmbG93X2lkGAYgASgJEhcKD2Zsb3dfZXhwaXJlc19hdBgHIAEoCRIgChhjbGllbnRfc2VjcmV0X2NvbmZpZ3VyZWQYCCABKAgi1AEKH0NvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAMgASgNEg8KB2Zsb3dfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSDAoEY29kZRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIUCgxyZWRpcmVjdF91cmkYCCABKAlCCQoHX2lzc3VlciJsChtSZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAMgASgNIuoDChhNY3BBcHByb3ZhbFJldmlld0NvbnRleHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgFIAEoCRIVCg1hcmd1bWVudF9oYXNoGAYgASgJEhQKDGJpbmRpbmdfaGFzaBgHIAEoCRIVCg1jb25uZWN0aW9uX2lkGAggASgJEhIKCmNhdGFsb2dfaWQYCSABKAkSGAoQY2F0YWxvZ19yZXZpc2lvbhgKIAEoDRIXCg9jb25maWdfcmV2aXNpb24YCyABKA0SFwoPYXV0aF9nZW5lcmF0aW9uGAwgASgNEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYDSABKAkSFQoNdXBzdHJlYW1fbmFtZRgOIAEoCRIYChB0b29sX2ZpbmdlcnByaW50GA8gASgJEhYKDmNvbnRleHRfZGlnZXN0GBAgASgJEhgKEGpvdXJuYWxfcmV2aXNpb24YESABKA0SEgoKZXhwaXJlc19hdBgSIAEoCRIXCg9wb2xpY3lfcmV2aXNpb24YEyABKAkSFQoNYmluZGluZ19lcG9jaBgUIAEoCSK/AQoSTWNwQXBwcm92YWxSZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghkZWNpc2lvbhgCIAEoCRIQCghhY3Rvcl9pZBgDIAEoCRISCgpkZWNpZGVkX2F0GAQgASgJEhgKEGpvdXJuYWxfcmV2aXNpb24YBSABKA0SQQoQcmV2aWV3ZWRfY29udGV4dBgGIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IuEEChJNY3BFeGVjdXRpb25SZXZpZXcSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhUKDWNvbm5lY3Rpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgEIAEoDRIaChJyZXN1bHRfdW5hdmFpbGFibGUYBSABKAgSKgoGcmV2aWV3GAYgASgLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbBIXCg9hbGxvd2VkX2FjdGlvbnMYByADKAkSDgoGcmVhc29uGAggASgJEhQKDHRvb2xfY2FsbF9pZBgJIAEoCRIOCgZydW5faWQYCiABKAkSFgoOcmVxdWVzdF9kaWdlc3QYCyABKAkSFQoNdXBzdHJlYW1fbmFtZRgMIAEoCRIZChFhcHByb3ZhbF9kZWNpc2lvbhgNIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYDiABKAkSLQoNcmV2aWV3ZWRfdG9vbBgPIAEoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBIqCiJhcmd1bWVudF9jb25zdHJhaW50X2RvY3VtZW50c19qc29uGBAgAygJEhQKDGJpbmRpbmdfaGFzaBgRIAEoCRJCChhleHBlY3RlZF91bmtub3duX2NvbnRleHQYEiABKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFVua25vd25Db250ZXh0EkMKFGNhbmNlbGxhdGlvbl9yZWNlaXB0GBMgASgLMiUuZW5ncmFtLmFwcC52MS5NY3BDYW5jZWxsYXRpb25SZWNlaXB0IoQBChFNY3BVbmtub3duQ29udGV4dBIUCgxleGVjdXRpb25faWQYASABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgCIAEoDRIWCg5yZXF1ZXN0X2RpZ2VzdBgDIAEoCRIUCgxiaW5kaW5nX2hhc2gYBCABKAkSEQoJaW50ZW50X2lkGAUgASgJIq8BChZNY3BDYW5jZWxsYXRpb25SZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRINCgVjYXVzZRgDIAEoCRIhChlleHBlY3RlZF9qb3VybmFsX3JldmlzaW9uGAQgASgNEhgKEGpvdXJuYWxfcmV2aXNpb24YBSABKA0SDQoFc3RhdGUYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCSKfAQoUTWNwUmVzb2x1dGlvblJlY2VpcHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEg8KB291dGNvbWUYAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoCRI6ChBleHBlY3RlZF9jb250ZXh0GAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BVbmtub3duQ29udGV4dCKBAQoRTWNwQmluZGluZ1JlY2VpcHQSFAoMYmluZGluZ19oYXNoGAEgASgJEhUKDWJpbmRpbmdfZXBvY2gYAiABKAkSDwoHY3VycmVudBgDIAEoCBIVCg1zdGFsZV9yZWFzb25zGAQgAygJEhcKD2FsbG93ZWRfYWN0aW9ucxgFIAMoCSKAAgoZR2V0U2Vzc2lvbk1jcFN0YXRlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhoKEmFmdGVyX2V4ZWN1dGlvbl9pZBgCIAEoCRIRCglwYWdlX3NpemUYAyABKA0SHgoRcGxhbl90b29sX2NhbGxfaWQYBCABKAlIAIgBARIfChdhZnRlcl9iaW5kaW5nX3Rvb2xfbmFtZRgFIAEoCRIZChFiaW5kaW5nX3BhZ2Vfc2l6ZRgGIAEoDRIVCg1vbWl0X2JpbmRpbmdzGAcgASgIEhcKD29taXRfZXhlY3V0aW9ucxgIIAEoCEIUChJfcGxhbl90b29sX2NhbGxfaWQivwQKGkdldFNlc3Npb25NY3BTdGF0ZVJlc3BvbnNlEjkKD2JpbmRpbmdfcmVjZWlwdBgBIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwQmluZGluZ1JlY2VpcHQSNQoKZXhlY3V0aW9ucxgCIAMoCzIhLmVuZ3JhbS5hcHAudjEuTWNwRXhlY3V0aW9uUmV2aWV3Ei0KCWFwcHJvdmFscxgDIAMoCzIaLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWwSHAoUdW5rbm93bl93cml0ZV9ibG9ja3MYBCADKAkSQAoTcmVzb2x1dGlvbl9yZWNlaXB0cxgFIAMoCzIjLmVuZ3JhbS5hcHAudjEuTWNwUmVzb2x1dGlvblJlY2VpcHQSFwoPYWxsb3dlZF9hY3Rpb25zGAYgAygJEhQKDGF2YWlsYWJpbGl0eRgHIAEoCRIZChFuZXh0X2V4ZWN1dGlvbl9pZBgIIAEoCRI+Cgx3cml0ZV9wb2xpY3kYCSABKAsyKC5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVzcG9uc2USOQoNYmluZGluZ190b29scxgKIAMoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQm91bmRUb29sU3VtbWFyeRIeChZuZXh0X2JpbmRpbmdfdG9vbF9uYW1lGAsgASgJEh4KFmhhc19tb3JlX2JpbmRpbmdfdG9vbHMYDCABKAgSGwoTaGFzX21vcmVfZXhlY3V0aW9ucxgNIAEoCCLKAgoTTWNwQm91bmRUb29sU3VtbWFyeRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhgKEGNvbm5lY3Rpb25fYWxpYXMYAiABKAkSFwoPbW9kZWxfdG9vbF9uYW1lGAMgASgJEi0KDXJldmlld2VkX3Rvb2wYBCABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgFIAMoCRIYChBhdXRvbWF0aW9uX3dyaXRlGAYgASgIEhQKDHVucmVzdHJpY3RlZBgHIAEoCBISCgpjYXRhbG9nX2lkGAggASgJEhgKEGNhdGFsb2dfcmV2aXNpb24YCSABKA0SFwoPY29uZmlnX3JldmlzaW9uGAogASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgLIAEoDSrIAQoUTWNwUmViaW5kRHJhaW5SZWFzb24SJwojTUNQX1JFQklORF9EUkFJTl9SRUFTT05fVU5TUEVDSUZJRUQQABIwCixNQ1BfUkVCSU5EX0RSQUlOX1JFQVNPTl9ESVNQQVRDSF9JTl9QUk9HUkVTUxABEioKJk1DUF9SRUJJTkRfRFJBSU5fUkVBU09OX1VOS05PV05fSU5URU5UEAISKQolTUNQX1JFQklORF9EUkFJTl9SRUFTT05fTEVHQUNZX0lOVEVOVBADMtUXChJJbnRlZ3JhdGlvblNlcnZpY2USXQoOTGlzdENvbm5lY3RvcnMSJC5lbmdyYW0uYXBwLnYxLkxpc3RDb25uZWN0b3JzUmVxdWVzdBolLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXNwb25zZRJgCg9VcHNlcnRDb25uZWN0b3ISJS5lbmdyYW0uYXBwLnYxLlVwc2VydENvbm5lY3RvclJlcXVlc3QaJi5lbmdyYW0uYXBwLnYxLlVwc2VydENvbm5lY3RvclJlc3BvbnNlEmAKD0RlbGV0ZUNvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2UScgoVR2V0SW50ZWdyYXRpb25DYXRhbG9nEisuZW5ncmFtLmFwcC52MS5HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXF1ZXN0GiwuZW5ncmFtLmFwcC52MS5HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXNwb25zZRJmChFTZXRNaW50Q3JlZGVudGlhbBInLmVuZ3JhbS5hcHAudjEuU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEmwKE1VwbG9hZENvbm5lY3RvckxvZ28SKS5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXF1ZXN0GiouZW5ncmFtLmFwcC52MS5VcGxvYWRDb25uZWN0b3JMb2dvUmVzcG9uc2USWgoNVGVzdENvbm5lY3RvchIjLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXNwb25zZRJvChRTZXRDb25uZWN0b3JTZXR0aW5ncxIqLmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0GisuZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1Jlc3BvbnNlEmAKD0xpc3RDb25uZWN0aW9ucxIlLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3Rpb25zUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3Rpb25zUmVzcG9uc2USYwoQQ3JlYXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkNyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRJjChBVcGRhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuVXBkYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEERlbGV0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0aW9uUmVzcG9uc2USXQoOVGVzdENvbm5lY3Rpb24SJC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0aW9uUmVxdWVzdBolLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXNwb25zZRJvChRTZXRDb25uZWN0aW9uRW5hYmxlZBIqLmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdGlvbkVuYWJsZWRSZXF1ZXN0GisuZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEmwKE0dldEdvb2dsZUNsb3VkU2V0dXASKS5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXF1ZXN0GiouZW5ncmFtLmFwcC52MS5HZXRHb29nbGVDbG91ZFNldHVwUmVzcG9uc2USVAoLRGlzY292ZXJNY3ASIS5lbmdyYW0uYXBwLnYxLkRpc2NvdmVyTWNwUmVxdWVzdBoiLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXNwb25zZRJRCgpQdWJsaXNoTWNwEiAuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVxdWVzdBohLmVuZ3JhbS5hcHAudjEuUHVibGlzaE1jcFJlc3BvbnNlEmMKEFB1dE1jcENyZWRlbnRpYWwSJi5lbmdyYW0uYXBwLnYxLlB1dE1jcENyZWRlbnRpYWxSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5QdXRNY3BDcmVkZW50aWFsUmVzcG9uc2USVAoLR2V0TWNwU2V0dXASIS5lbmdyYW0uYXBwLnYxLkdldE1jcFNldHVwUmVxdWVzdBoiLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXNwb25zZRJ7ChhDb21wbGV0ZU1jcEF1dGhvcml6YXRpb24SLi5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlcXVlc3QaLy5lbmdyYW0uYXBwLnYxLkNvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlc3BvbnNlEm8KFFJlY292ZXJNY3BDcmVkZW50aWFsEiouZW5ncmFtLmFwcC52MS5SZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QaKy5lbmdyYW0uYXBwLnYxLlJlY292ZXJNY3BDcmVkZW50aWFsUmVzcG9uc2USVwoMQXV0aG9yaXplTWNwEiIuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXF1ZXN0GiMuZW5ncmFtLmFwcC52MS5BdXRob3JpemVNY3BSZXNwb25zZRJaCg1EaXNjb25uZWN0TWNwEiMuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVxdWVzdBokLmVuZ3JhbS5hcHAudjEuRGlzY29ubmVjdE1jcFJlc3BvbnNlEmkKEkdldFNlc3Npb25NY3BTdGF0ZRIoLmVuZ3JhbS5hcHAudjEuR2V0U2Vzc2lvbk1jcFN0YXRlUmVxdWVzdBopLmVuZ3JhbS5hcHAudjEuR2V0U2Vzc2lvbk1jcFN0YXRlUmVzcG9uc2USYwoQTGlzdE1jcEFwcHJvdmFscxImLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1JlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkxpc3RNY3BBcHByb3ZhbHNSZXNwb25zZRJmChFEZWNpZGVNY3BBcHByb3ZhbBInLmVuZ3JhbS5hcHAudjEuRGVjaWRlTWNwQXBwcm92YWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlc3BvbnNlEloKDUNhbmNlbE1jcENhbGwSIy5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5DYW5jZWxNY3BDYWxsUmVzcG9uc2USZgoRUmVzb2x2ZU1jcE91dGNvbWUSJy5lbmdyYW0uYXBwLnYxLlJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBooLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXNwb25zZRJjChBQcmVwYXJlTWNwUmViaW5kEiYuZW5ncmFtLmFwcC52MS5QcmVwYXJlTWNwUmViaW5kUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuUHJlcGFyZU1jcFJlYmluZFJlc3BvbnNlEk4KCVJlYmluZE1jcBIfLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVxdWVzdBogLmVuZ3JhbS5hcHAudjEuUmViaW5kTWNwUmVzcG9uc2ViBnByb3RvMw", [file_engram_app_v1_session]); + fileDesc("Ch9lbmdyYW0vYXBwL3YxL2ludGVncmF0aW9uLnByb3RvEg1lbmdyYW0uYXBwLnYxIpgBChtHb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSIgoad29ya2xvYWRfaWRlbnRpdHlfcHJvdmlkZXIYASABKAkSHQoVc2VydmljZV9hY2NvdW50X2VtYWlsGAIgASgJEhEKCWVuZHBvaW50cxgDIAMoCRIjChtjbG91ZF9zcWxfcG9zdGdyZXNfaW5zdGFuY2UYBCABKAkipwMKFUludGVncmF0aW9uQ29ubmVjdGlvbhIKCgJpZBgBIAEoCRINCgVhbGlhcxgCIAEoCRIQCghwcm92aWRlchgDIAEoCRIUCgxkaXNwbGF5X25hbWUYBCABKAkSDwoHZW5hYmxlZBgFIAEoCBIRCgl0ZXN0ZWRfYXQYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCRISCgp1cGRhdGVkX2F0GAggASgJEkAKDGdvb2dsZV9jbG91ZBgJIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEhIKCmlzX2RlZmF1bHQYCiABKAgSLwoDbWNwGAsgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnEhcKD2NvbmZpZ19yZXZpc2lvbhgMIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YDSABKA0SGwoTY3JlZGVudGlhbF9pZGVudGl0eRgOIAEoCRIYChBjYXRhbG9nX3JldmlzaW9uGA8gASgNEg8KB3Jldm9rZWQYECABKAgiGAoWTGlzdENvbm5lY3Rpb25zUmVxdWVzdCJUChdMaXN0Q29ubmVjdGlvbnNSZXNwb25zZRI5Cgtjb25uZWN0aW9ucxgBIAMoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIsMBChdDcmVhdGVDb25uZWN0aW9uUmVxdWVzdBINCgVhbGlhcxgBIAEoCRIQCghwcm92aWRlchgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSQAoMZ29vZ2xlX2Nsb3VkGAQgASgLMiouZW5ncmFtLmFwcC52MS5Hb29nbGVDbG91ZENvbm5lY3Rpb25Db25maWcSLwoDbWNwGAUgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDb25uZWN0aW9uQ29uZmlnIlQKGENyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24i9QEKF1VwZGF0ZUNvbm5lY3Rpb25SZXF1ZXN0EhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YByABKA0SGQoRZXhwZWN0ZWRfcmV2aXNpb24YBiABKA0SCgoCaWQYASABKAkSDQoFYWxpYXMYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEkAKDGdvb2dsZV9jbG91ZBgEIAEoCzIqLmVuZ3JhbS5hcHAudjEuR29vZ2xlQ2xvdWRDb25uZWN0aW9uQ29uZmlnEi8KA21jcBgFIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ29ubmVjdGlvbkNvbmZpZyJUChhVcGRhdGVDb25uZWN0aW9uUmVzcG9uc2USOAoKY29ubmVjdGlvbhgBIAEoCzIkLmVuZ3JhbS5hcHAudjEuSW50ZWdyYXRpb25Db25uZWN0aW9uIl0KF0RlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAIgASgNEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAyABKA0SCgoCaWQYASABKAkiKwoYRGVsZXRlQ29ubmVjdGlvblJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKAgiIwoVVGVzdENvbm5lY3Rpb25SZXF1ZXN0EgoKAmlkGAEgASgJIjUKFlRlc3RDb25uZWN0aW9uUmVzcG9uc2USCgoCb2sYASABKAgSDwoHbWVzc2FnZRgCIAEoCSJVChtTZXRDb25uZWN0aW9uRW5hYmxlZFJlcXVlc3QSCgoCaWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDSJYChxTZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiIoChpHZXRHb29nbGVDbG91ZFNldHVwUmVxdWVzdBIKCgJpZBgBIAEoCSKiAQobR2V0R29vZ2xlQ2xvdWRTZXR1cFJlc3BvbnNlEg4KBmlzc3VlchgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIZChFzdWJqZWN0X2F0dHJpYnV0ZRgDIAEoCRIcChRjb25uZWN0aW9uX2F0dHJpYnV0ZRgEIAEoCRIVCg1nY2xvdWRfc2NyaXB0GAUgASgJEhEKCXRlcnJhZm9ybRgGIAEoCSLmAQoJQ29ubmVjdG9yEhAKCHByb3ZpZGVyGAEgASgJEhMKC2NvbmZpZ19qc29uGAIgASgJEg8KB2J1aWx0aW4YAyABKAgSEgoKY3JlYXRlZF9hdBgEIAEoCRISCgp1cGRhdGVkX2F0GAUgASgJEg4KBnN0YXR1cxgGIAEoCRI4CghzZXR0aW5ncxgHIAMoCzImLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yLlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIhcKFUxpc3RDb25uZWN0b3JzUmVxdWVzdCJGChZMaXN0Q29ubmVjdG9yc1Jlc3BvbnNlEiwKCmNvbm5lY3RvcnMYASADKAsyGC5lbmdyYW0uYXBwLnYxLkNvbm5lY3RvciItChZVcHNlcnRDb25uZWN0b3JSZXF1ZXN0EhMKC2NvbmZpZ19qc29uGAEgASgJIkYKF1Vwc2VydENvbm5lY3RvclJlc3BvbnNlEisKCWNvbm5lY3RvchgBIAEoCzIYLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yIioKFkRlbGV0ZUNvbm5lY3RvclJlcXVlc3QSEAoIcHJvdmlkZXIYASABKAkiKgoXRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2USDwoHZGVsZXRlZBgBIAEoCCI6Cg1Db25uZWN0b3JJY29uEgwKBG1vbm8YASABKAkSDQoFY29sb3IYAiABKAkSDAoEbG9nbxgDIAEoCSJ/ChBDb25uZWN0b3JEaXNwbGF5EgwKBG5hbWUYASABKAkSEAoIY2F0ZWdvcnkYAiABKAkSDQoFYmx1cmIYAyABKAkSKgoEaWNvbhgEIAEoCzIcLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9ySWNvbhIQCghmZWF0dXJlZBgFIAEoCCJ2ChFDYXRhbG9nQ2FwYWJpbGl0eRIOCgZhY3Rpb24YASABKAkSDgoGYWNjZXNzGAIgASgJEg0KBWFzc2V0GAMgASgJEg0KBWxhYmVsGAQgASgJEgwKBGhvc3QYBSABKAkSFQoNZW5kcG9pbnRfcnVsZRgGIAEoCSK0AgoUUHJvdmlkZXJDYXRhbG9nRW50cnkSEAoIcHJvdmlkZXIYASABKAkSMAoHZGlzcGxheRgCIAEoCzIfLmVuZ3JhbS5hcHAudjEuQ29ubmVjdG9yRGlzcGxheRIZChFjcmVkZW50aWFsX3NvdXJjZRgDIAEoCRINCgVob3N0cxgEIAMoCRI2CgxjYXBhYmlsaXRpZXMYBSADKAsyIC5lbmdyYW0uYXBwLnYxLkNhdGFsb2dDYXBhYmlsaXR5Eh0KFWRlZmF1bHRfY29ubmVjdGlvbl9pZBgGIAEoCRIYChBjb25uZWN0aW9uX21vZGVsGAcgASgJEj0KD3VzZXJfY3JlZGVudGlhbBgIIAEoCzIkLmVuZ3JhbS5hcHAudjEuVXNlckNyZWRlbnRpYWxTdXBwb3J0IkkKFVVzZXJDcmVkZW50aWFsU3VwcG9ydBINCgVvYXV0aBgBIAEoCBINCgV0b2tlbhgCIAEoCBISCgp0b2tlbl9oaW50GAMgASgJIh4KHEdldEludGVncmF0aW9uQ2F0YWxvZ1JlcXVlc3QiVwodR2V0SW50ZWdyYXRpb25DYXRhbG9nUmVzcG9uc2USNgoJcHJvdmlkZXJzGAEgAygLMiMuZW5ncmFtLmFwcC52MS5Qcm92aWRlckNhdGFsb2dFbnRyeSKuAQoYU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0EhAKCHByb3ZpZGVyGAEgASgJEgwKBGtpbmQYAiABKAkSQwoGdmFsdWVzGAMgAygLMjMuZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlcXVlc3QuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChlTZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEhQKDHNlY3JldF9uYW1lcxgBIAMoCSJQChpVcGxvYWRDb25uZWN0b3JMb2dvUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRIMCgRkYXRhGAIgASgMEhIKCm1lZGlhX3R5cGUYAyABKAkiLwobVXBsb2FkQ29ubmVjdG9yTG9nb1Jlc3BvbnNlEhAKCGxvZ29fdXJsGAEgASgJIq4CChRUZXN0Q29ubmVjdG9yUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCgxkcmFmdF92YWx1ZXMYAiADKAsyNC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0VmFsdWVzRW50cnkSTgoOZHJhZnRfc2V0dGluZ3MYAyADKAsyNi5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXF1ZXN0LkRyYWZ0U2V0dGluZ3NFbnRyeRoyChBEcmFmdFZhbHVlc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaNAoSRHJhZnRTZXR0aW5nc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoVVGVzdENvbm5lY3RvclJlc3BvbnNlEgoKAm9rGAEgASgIEg8KB21lc3NhZ2UYAiABKAkirAEKG1NldENvbm5lY3RvclNldHRpbmdzUmVxdWVzdBIQCghwcm92aWRlchgBIAEoCRJKCghzZXR0aW5ncxgCIAMoCzI4LmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0LlNldHRpbmdzRW50cnkaLwoNU2V0dGluZ3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIksKHFNldENvbm5lY3RvclNldHRpbmdzUmVzcG9uc2USKwoJY29ubmVjdG9yGAEgASgLMhguZW5ncmFtLmFwcC52MS5Db25uZWN0b3IiqwEKE01jcENvbm5lY3Rpb25Db25maWcSEAoIZW5kcG9pbnQYASABKAkSDAoEem9uZRgCIAEoCRIRCglhdXRoX2tpbmQYAyABKAkSGgoSYXBpX2tleV9zZWNyZXRfcmVmGAQgASgJEiQKHG9hdXRoX2NsaWVudF9yZWdpc3RyYXRpb25faWQYBSABKAkSHwoXc2hhcmVfd2l0aF9vcmdfc2Vzc2lvbnMYBiABKAgiqgEKB01jcFRvb2wSFQoNdXBzdHJlYW1fbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIZChFpbnB1dF9zY2hlbWFfanNvbhgDIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YBCABKAkSFgoOY2xhc3NpZmljYXRpb24YBSABKAkSDwoHZW5hYmxlZBgGIAEoCBITCgtmaW5nZXJwcmludBgHIAEoCSK9AQoKTWNwQ2F0YWxvZxIKCgJpZBgBIAEoCRIVCg1jb25uZWN0aW9uX2lkGAIgASgJEgwKBGtpbmQYAyABKAkSEAoIcmV2aXNpb24YBCABKA0SFwoPY29uZmlnX3JldmlzaW9uGAUgASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgGIAEoDRIlCgV0b29scxgHIAMoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBITCgtmaW5nZXJwcmludBgIIAEoCSIuChVHZXRNY3BDYXRhbG9nc1JlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCSKjAQoWR2V0TWNwQ2F0YWxvZ3NSZXNwb25zZRIXCg9jb25maWdfcmV2aXNpb24YASABKA0SGAoQY2F0YWxvZ19yZXZpc2lvbhgCIAEoDRIoCgVkcmFmdBgDIAEoCzIZLmVuZ3JhbS5hcHAudjEuTWNwQ2F0YWxvZxIsCglwdWJsaXNoZWQYBCABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2ciRgoSRGlzY292ZXJNY3BSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAiABKA0iQQoTRGlzY292ZXJNY3BSZXNwb25zZRIqCgdjYXRhbG9nGAEgASgLMhkuZW5ncmFtLmFwcC52MS5NY3BDYXRhbG9nIn4KEVB1Ymxpc2hNY3BSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAkSEAoIZHJhZnRfaWQYAiABKAkSGQoRZXhwZWN0ZWRfcmV2aXNpb24YAyABKA0SJQoFdG9vbHMYBCADKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wiQAoSUHVibGlzaE1jcFJlc3BvbnNlEioKB2NhdGFsb2cYASABKAsyGS5lbmdyYW0uYXBwLnYxLk1jcENhdGFsb2cihQEKE0F1dGhvcml6ZU1jcFJlcXVlc3QSGQoRZXhwZWN0ZWRfcmV2aXNpb24YBCABKA0SFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAIgASgNEh8KF3NoYXJlX3dpdGhfb3JnX3Nlc3Npb25zGAMgASgIIogBChRBdXRob3JpemVNY3BSZXNwb25zZRIXCg9jb25maWdfcmV2aXNpb24YBCABKA0SFwoPYXV0aF9nZW5lcmF0aW9uGAUgASgNEg8KB2Zsb3dfaWQYAiABKAkSEgoKZXhwaXJlc19hdBgDIAEoCRIZChFhdXRob3JpemF0aW9uX3VybBgBIAEoCSJlChREaXNjb25uZWN0TWNwUmVxdWVzdBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoDRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhsKE2V4cGVjdGVkX2dlbmVyYXRpb24YAiABKA0iUQoVRGlzY29ubmVjdE1jcFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbiKPAwoLTWNwQXBwcm92YWwSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRIVCg1jb25uZWN0aW9uX2lkGAUgASgJEhUKDXVwc3RyZWFtX25hbWUYBiABKAkSFgoOcmVxdWVzdF9kaWdlc3QYByABKAkSFQoNYXJndW1lbnRfaGFzaBgIIAEoCRISCgpleHBpcmVzX2F0GAkgASgJEhoKEm9yaWdpbmFsX2FyZ3VtZW50cxgKIAEoCRItCg1yZXZpZXdlZF90b29sGAsgASgLMhYuZW5ncmFtLmFwcC52MS5NY3BUb29sEioKImFyZ3VtZW50X2NvbnN0cmFpbnRfZG9jdW1lbnRzX2pzb24YDCADKAkSSAoXZXhwZWN0ZWRfcmV2aWV3X2NvbnRleHQYDSABKAsyJy5lbmdyYW0uYXBwLnYxLk1jcEFwcHJvdmFsUmV2aWV3Q29udGV4dCJJChdMaXN0TWNwQXBwcm92YWxzUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhoKEmFmdGVyX2V4ZWN1dGlvbl9pZBgCIAEoCSJkChhMaXN0TWNwQXBwcm92YWxzUmVzcG9uc2USLQoJYXBwcm92YWxzGAEgAygLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbBIZChFuZXh0X2V4ZWN1dGlvbl9pZBgCIAEoCSKLAQoYRGVjaWRlTWNwQXBwcm92YWxSZXF1ZXN0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIPCgdhcHByb3ZlGAIgASgIEkgKF2V4cGVjdGVkX3Jldmlld19jb250ZXh0GAMgASgLMicuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbFJldmlld0NvbnRleHQiXgoZRGVjaWRlTWNwQXBwcm92YWxSZXNwb25zZRINCgVzdGF0ZRgBIAEoCRIyCgdyZWNlaXB0GAIgASgLMiEuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbFJlY2VpcHQiTwoUQ2FuY2VsTWNwQ2FsbFJlcXVlc3QSFAoMZXhlY3V0aW9uX2lkGAEgASgJEiEKGWV4cGVjdGVkX2pvdXJuYWxfcmV2aXNpb24YAiABKA0iXgoVQ2FuY2VsTWNwQ2FsbFJlc3BvbnNlEg0KBXN0YXRlGAEgASgJEjYKB3JlY2VpcHQYAiABKAsyJS5lbmdyYW0uYXBwLnYxLk1jcENhbmNlbGxhdGlvblJlY2VpcHQihQEKGFJlc29sdmVNY3BPdXRjb21lUmVxdWVzdBIUCgxleGVjdXRpb25faWQYASABKAkSDwoHb3V0Y29tZRgCIAEoCRJCChhleHBlY3RlZF91bmtub3duX2NvbnRleHQYAyABKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFVua25vd25Db250ZXh0ImIKGVJlc29sdmVNY3BPdXRjb21lUmVzcG9uc2USDwoHb3V0Y29tZRgBIAEoCRI0CgdyZWNlaXB0GAIgASgLMiMuZW5ncmFtLmFwcC52MS5NY3BSZXNvbHV0aW9uUmVjZWlwdCJMChdQcmVwYXJlTWNwUmViaW5kUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWV4cGVjdGVkX2JpbmRpbmdfaGFzaBgCIAEoCSKpAgoYUHJlcGFyZU1jcFJlYmluZFJlc3BvbnNlEhQKDGJpbmRpbmdfaGFzaBgBIAEoCRIVCg1iaW5kaW5nX2Vwb2NoGAIgASgJEjkKDWJpbmRpbmdfdG9vbHMYAyADKAsyIi5lbmdyYW0uYXBwLnYxLk1jcEJvdW5kVG9vbFN1bW1hcnkSEgoKZGVueV9ob3N0cxgEIAMoCRIVCg1kZW55X3JldmlzaW9uGAUgASgJEhMKC2RlbnlfZGlnZXN0GAYgASgJEjcKC2Nvbm5lY3Rpb25zGAcgAygLMiIuZW5ncmFtLmFwcC52MS5NY3BSZWJpbmRDb25uZWN0aW9uEiwKBWRyYWluGAggASgLMh0uZW5ncmFtLmFwcC52MS5NY3BSZWJpbmREcmFpbiJaCg5NY3BSZWJpbmREcmFpbhIzCgZyZWFzb24YASABKA4yIy5lbmdyYW0uYXBwLnYxLk1jcFJlYmluZERyYWluUmVhc29uEhMKC3JldHJ5X2FmdGVyGAIgASgJIsQBChNNY3BSZWJpbmRDb25uZWN0aW9uEhUKDWNvbm5lY3Rpb25faWQYASABKAkSDQoFYWxpYXMYAiABKAkSEAoIZW5kcG9pbnQYAyABKAkSDAoEem9uZRgEIAEoCRIbChNjcmVkZW50aWFsX2lkZW50aXR5GAUgASgJEhcKD2NvbmZpZ19yZXZpc2lvbhgGIAEoDRIXCg9hdXRoX2dlbmVyYXRpb24YByABKA0SGAoQY2F0YWxvZ19yZXZpc2lvbhgIIAEoDSJmChBSZWJpbmRNY3BSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSHQoVZXhwZWN0ZWRfYmluZGluZ19oYXNoGAIgASgJEh8KF2V4cGVjdGVkX2NhbmRpZGF0ZV9oYXNoGAMgASgJIikKEVJlYmluZE1jcFJlc3BvbnNlEhQKDGJpbmRpbmdfaGFzaBgBIAEoCSKVAQoXUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAMgASgNEgwKBGtpbmQYBCABKAkSDgoGaGVhZGVyGAUgASgJEg0KBXZhbHVlGAYgASgJIpQBChNNY3BDcmVkZW50aWFsU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIRCglyZWZlcmVuY2UYAiABKAkSDAoEa2luZBgDIAEoCRIbChNjcmVkZW50aWFsX2lkZW50aXR5GAQgASgJEg8KB3ZlcnNpb24YBSABKAMSHgoWcmVzZXJ2YXRpb25fZ2VuZXJhdGlvbhgGIAEoAyKMAQoYUHV0TWNwQ3JlZGVudGlhbFJlc3BvbnNlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbhI2CgpjcmVkZW50aWFsGAIgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdHVzIpQBCiBDb21wbGV0ZU1jcEF1dGhvcml6YXRpb25SZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyKQAQocUmVjb3Zlck1jcENyZWRlbnRpYWxSZXNwb25zZRI4Cgpjb25uZWN0aW9uGAEgASgLMiQuZW5ncmFtLmFwcC52MS5JbnRlZ3JhdGlvbkNvbm5lY3Rpb24SNgoKY3JlZGVudGlhbBgCIAEoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQ3JlZGVudGlhbFN0YXR1cyKGAQoSTWNwQ3JlZGVudGlhbFN0YXRlEjgKCmNvbm5lY3Rpb24YASABKAsyJC5lbmdyYW0uYXBwLnYxLkludGVncmF0aW9uQ29ubmVjdGlvbhI2CgpjcmVkZW50aWFsGAIgASgLMiIuZW5ncmFtLmFwcC52MS5NY3BDcmVkZW50aWFsU3RhdHVzIjsKFk1jcEFwcHJvdmVkRGVzdGluYXRpb24SDwoHem9uZV9pZBgBIAEoCRIQCghlbmRwb2ludBgCIAEoCSK7AQoVTWNwQ2xpZW50UmVnaXN0cmF0aW9uEgoKAmlkGAEgASgJEhEKCWNsaWVudF9pZBgCIAEoCRIUCgxjb25maWRlbnRpYWwYAyABKAgSFAoMcmVkaXJlY3RfdXJpGAQgASgJEg4KBmlzc3VlchgFIAEoCRI3CghyZXNvdXJjZRgGIAEoCzIlLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92ZWREZXN0aW5hdGlvbhIOCgZzY29wZXMYByADKAkiKwoSR2V0TWNwU2V0dXBSZXF1ZXN0EhUKDWNvbm5lY3Rpb25faWQYASABKAki2QIKE0dldE1jcFNldHVwUmVzcG9uc2USDwoHZW5hYmxlZBgBIAEoCBI7Cg1yZWdpc3RyYXRpb25zGAIgAygLMiQuZW5ncmFtLmFwcC52MS5NY3BDbGllbnRSZWdpc3RyYXRpb24SOwoMZGVzdGluYXRpb25zGAMgAygLMiUuZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZlZERlc3RpbmF0aW9uEjIKB2N1cnJlbnQYBCABKAsyIS5lbmdyYW0uYXBwLnYxLk1jcENyZWRlbnRpYWxTdGF0ZRIaChJ0cmFuc2l0aW9uX3BlbmRpbmcYBSABKAgSDwoHZmxvd19pZBgGIAEoCRIXCg9mbG93X2V4cGlyZXNfYXQYByABKAkSIAoYY2xpZW50X3NlY3JldF9jb25maWd1cmVkGAggASgIEhsKE2Rpc2NvdmVyeV9hdmFpbGFibGUYCSABKAgi1AEKH0NvbXBsZXRlTWNwQXV0aG9yaXphdGlvblJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAMgASgNEg8KB2Zsb3dfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSDAoEY29kZRgGIAEoCRITCgZpc3N1ZXIYByABKAlIAIgBARIUCgxyZWRpcmVjdF91cmkYCCABKAlCCQoHX2lzc3VlciJsChtSZWNvdmVyTWNwQ3JlZGVudGlhbFJlcXVlc3QSFQoNY29ubmVjdGlvbl9pZBgBIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgCIAEoDRIbChNleHBlY3RlZF9nZW5lcmF0aW9uGAMgASgNIuoDChhNY3BBcHByb3ZhbFJldmlld0NvbnRleHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSDgoGcnVuX2lkGAMgASgJEhQKDHRvb2xfY2FsbF9pZBgEIAEoCRIWCg5yZXF1ZXN0X2RpZ2VzdBgFIAEoCRIVCg1hcmd1bWVudF9oYXNoGAYgASgJEhQKDGJpbmRpbmdfaGFzaBgHIAEoCRIVCg1jb25uZWN0aW9uX2lkGAggASgJEhIKCmNhdGFsb2dfaWQYCSABKAkSGAoQY2F0YWxvZ19yZXZpc2lvbhgKIAEoDRIXCg9jb25maWdfcmV2aXNpb24YCyABKA0SFwoPYXV0aF9nZW5lcmF0aW9uGAwgASgNEhsKE2NyZWRlbnRpYWxfaWRlbnRpdHkYDSABKAkSFQoNdXBzdHJlYW1fbmFtZRgOIAEoCRIYChB0b29sX2ZpbmdlcnByaW50GA8gASgJEhYKDmNvbnRleHRfZGlnZXN0GBAgASgJEhgKEGpvdXJuYWxfcmV2aXNpb24YESABKA0SEgoKZXhwaXJlc19hdBgSIAEoCRIXCg9wb2xpY3lfcmV2aXNpb24YEyABKAkSFQoNYmluZGluZ19lcG9jaBgUIAEoCSK/AQoSTWNwQXBwcm92YWxSZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghkZWNpc2lvbhgCIAEoCRIQCghhY3Rvcl9pZBgDIAEoCRISCgpkZWNpZGVkX2F0GAQgASgJEhgKEGpvdXJuYWxfcmV2aXNpb24YBSABKA0SQQoQcmV2aWV3ZWRfY29udGV4dBgGIAEoCzInLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWxSZXZpZXdDb250ZXh0IuEEChJNY3BFeGVjdXRpb25SZXZpZXcSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhUKDWNvbm5lY3Rpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgEIAEoDRIaChJyZXN1bHRfdW5hdmFpbGFibGUYBSABKAgSKgoGcmV2aWV3GAYgASgLMhouZW5ncmFtLmFwcC52MS5NY3BBcHByb3ZhbBIXCg9hbGxvd2VkX2FjdGlvbnMYByADKAkSDgoGcmVhc29uGAggASgJEhQKDHRvb2xfY2FsbF9pZBgJIAEoCRIOCgZydW5faWQYCiABKAkSFgoOcmVxdWVzdF9kaWdlc3QYCyABKAkSFQoNdXBzdHJlYW1fbmFtZRgMIAEoCRIZChFhcHByb3ZhbF9kZWNpc2lvbhgNIAEoCRIaChJvcmlnaW5hbF9hcmd1bWVudHMYDiABKAkSLQoNcmV2aWV3ZWRfdG9vbBgPIAEoCzIWLmVuZ3JhbS5hcHAudjEuTWNwVG9vbBIqCiJhcmd1bWVudF9jb25zdHJhaW50X2RvY3VtZW50c19qc29uGBAgAygJEhQKDGJpbmRpbmdfaGFzaBgRIAEoCRJCChhleHBlY3RlZF91bmtub3duX2NvbnRleHQYEiABKAsyIC5lbmdyYW0uYXBwLnYxLk1jcFVua25vd25Db250ZXh0EkMKFGNhbmNlbGxhdGlvbl9yZWNlaXB0GBMgASgLMiUuZW5ncmFtLmFwcC52MS5NY3BDYW5jZWxsYXRpb25SZWNlaXB0IoQBChFNY3BVbmtub3duQ29udGV4dBIUCgxleGVjdXRpb25faWQYASABKAkSGAoQam91cm5hbF9yZXZpc2lvbhgCIAEoDRIWCg5yZXF1ZXN0X2RpZ2VzdBgDIAEoCRIUCgxiaW5kaW5nX2hhc2gYBCABKAkSEQoJaW50ZW50X2lkGAUgASgJIq8BChZNY3BDYW5jZWxsYXRpb25SZWNlaXB0EhQKDGV4ZWN1dGlvbl9pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRINCgVjYXVzZRgDIAEoCRIhChlleHBlY3RlZF9qb3VybmFsX3JldmlzaW9uGAQgASgNEhgKEGpvdXJuYWxfcmV2aXNpb24YBSABKA0SDQoFc3RhdGUYBiABKAkSEgoKY3JlYXRlZF9hdBgHIAEoCSKfAQoUTWNwUmVzb2x1dGlvblJlY2VpcHQSFAoMZXhlY3V0aW9uX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEg8KB291dGNvbWUYAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoCRI6ChBleHBlY3RlZF9jb250ZXh0GAUgASgLMiAuZW5ncmFtLmFwcC52MS5NY3BVbmtub3duQ29udGV4dCKBAQoRTWNwQmluZGluZ1JlY2VpcHQSFAoMYmluZGluZ19oYXNoGAEgASgJEhUKDWJpbmRpbmdfZXBvY2gYAiABKAkSDwoHY3VycmVudBgDIAEoCBIVCg1zdGFsZV9yZWFzb25zGAQgAygJEhcKD2FsbG93ZWRfYWN0aW9ucxgFIAMoCSKAAgoZR2V0U2Vzc2lvbk1jcFN0YXRlUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEhoKEmFmdGVyX2V4ZWN1dGlvbl9pZBgCIAEoCRIRCglwYWdlX3NpemUYAyABKA0SHgoRcGxhbl90b29sX2NhbGxfaWQYBCABKAlIAIgBARIfChdhZnRlcl9iaW5kaW5nX3Rvb2xfbmFtZRgFIAEoCRIZChFiaW5kaW5nX3BhZ2Vfc2l6ZRgGIAEoDRIVCg1vbWl0X2JpbmRpbmdzGAcgASgIEhcKD29taXRfZXhlY3V0aW9ucxgIIAEoCEIUChJfcGxhbl90b29sX2NhbGxfaWQivwQKGkdldFNlc3Npb25NY3BTdGF0ZVJlc3BvbnNlEjkKD2JpbmRpbmdfcmVjZWlwdBgBIAEoCzIgLmVuZ3JhbS5hcHAudjEuTWNwQmluZGluZ1JlY2VpcHQSNQoKZXhlY3V0aW9ucxgCIAMoCzIhLmVuZ3JhbS5hcHAudjEuTWNwRXhlY3V0aW9uUmV2aWV3Ei0KCWFwcHJvdmFscxgDIAMoCzIaLmVuZ3JhbS5hcHAudjEuTWNwQXBwcm92YWwSHAoUdW5rbm93bl93cml0ZV9ibG9ja3MYBCADKAkSQAoTcmVzb2x1dGlvbl9yZWNlaXB0cxgFIAMoCzIjLmVuZ3JhbS5hcHAudjEuTWNwUmVzb2x1dGlvblJlY2VpcHQSFwoPYWxsb3dlZF9hY3Rpb25zGAYgAygJEhQKDGF2YWlsYWJpbGl0eRgHIAEoCRIZChFuZXh0X2V4ZWN1dGlvbl9pZBgIIAEoCRI+Cgx3cml0ZV9wb2xpY3kYCSABKAsyKC5lbmdyYW0uYXBwLnYxLkdldE1jcFdyaXRlUG9saWN5UmVzcG9uc2USOQoNYmluZGluZ190b29scxgKIAMoCzIiLmVuZ3JhbS5hcHAudjEuTWNwQm91bmRUb29sU3VtbWFyeRIeChZuZXh0X2JpbmRpbmdfdG9vbF9uYW1lGAsgASgJEh4KFmhhc19tb3JlX2JpbmRpbmdfdG9vbHMYDCABKAgSGwoTaGFzX21vcmVfZXhlY3V0aW9ucxgNIAEoCCLKAgoTTWNwQm91bmRUb29sU3VtbWFyeRIVCg1jb25uZWN0aW9uX2lkGAEgASgJEhgKEGNvbm5lY3Rpb25fYWxpYXMYAiABKAkSFwoPbW9kZWxfdG9vbF9uYW1lGAMgASgJEi0KDXJldmlld2VkX3Rvb2wYBCABKAsyFi5lbmdyYW0uYXBwLnYxLk1jcFRvb2wSKgoiYXJndW1lbnRfY29uc3RyYWludF9kb2N1bWVudHNfanNvbhgFIAMoCRIYChBhdXRvbWF0aW9uX3dyaXRlGAYgASgIEhQKDHVucmVzdHJpY3RlZBgHIAEoCBISCgpjYXRhbG9nX2lkGAggASgJEhgKEGNhdGFsb2dfcmV2aXNpb24YCSABKA0SFwoPY29uZmlnX3JldmlzaW9uGAogASgNEhcKD2F1dGhfZ2VuZXJhdGlvbhgLIAEoDSrIAQoUTWNwUmViaW5kRHJhaW5SZWFzb24SJwojTUNQX1JFQklORF9EUkFJTl9SRUFTT05fVU5TUEVDSUZJRUQQABIwCixNQ1BfUkVCSU5EX0RSQUlOX1JFQVNPTl9ESVNQQVRDSF9JTl9QUk9HUkVTUxABEioKJk1DUF9SRUJJTkRfRFJBSU5fUkVBU09OX1VOS05PV05fSU5URU5UEAISKQolTUNQX1JFQklORF9EUkFJTl9SRUFTT05fTEVHQUNZX0lOVEVOVBADMrQYChJJbnRlZ3JhdGlvblNlcnZpY2USXQoOTGlzdENvbm5lY3RvcnMSJC5lbmdyYW0uYXBwLnYxLkxpc3RDb25uZWN0b3JzUmVxdWVzdBolLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3RvcnNSZXNwb25zZRJgCg9VcHNlcnRDb25uZWN0b3ISJS5lbmdyYW0uYXBwLnYxLlVwc2VydENvbm5lY3RvclJlcXVlc3QaJi5lbmdyYW0uYXBwLnYxLlVwc2VydENvbm5lY3RvclJlc3BvbnNlEmAKD0RlbGV0ZUNvbm5lY3RvchIlLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdG9yUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuRGVsZXRlQ29ubmVjdG9yUmVzcG9uc2UScgoVR2V0SW50ZWdyYXRpb25DYXRhbG9nEisuZW5ncmFtLmFwcC52MS5HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXF1ZXN0GiwuZW5ncmFtLmFwcC52MS5HZXRJbnRlZ3JhdGlvbkNhdGFsb2dSZXNwb25zZRJmChFTZXRNaW50Q3JlZGVudGlhbBInLmVuZ3JhbS5hcHAudjEuU2V0TWludENyZWRlbnRpYWxSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5TZXRNaW50Q3JlZGVudGlhbFJlc3BvbnNlEmwKE1VwbG9hZENvbm5lY3RvckxvZ28SKS5lbmdyYW0uYXBwLnYxLlVwbG9hZENvbm5lY3RvckxvZ29SZXF1ZXN0GiouZW5ncmFtLmFwcC52MS5VcGxvYWRDb25uZWN0b3JMb2dvUmVzcG9uc2USWgoNVGVzdENvbm5lY3RvchIjLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3RvclJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0b3JSZXNwb25zZRJvChRTZXRDb25uZWN0b3JTZXR0aW5ncxIqLmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdG9yU2V0dGluZ3NSZXF1ZXN0GisuZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0b3JTZXR0aW5nc1Jlc3BvbnNlEmAKD0xpc3RDb25uZWN0aW9ucxIlLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3Rpb25zUmVxdWVzdBomLmVuZ3JhbS5hcHAudjEuTGlzdENvbm5lY3Rpb25zUmVzcG9uc2USYwoQQ3JlYXRlQ29ubmVjdGlvbhImLmVuZ3JhbS5hcHAudjEuQ3JlYXRlQ29ubmVjdGlvblJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLkNyZWF0ZUNvbm5lY3Rpb25SZXNwb25zZRJjChBVcGRhdGVDb25uZWN0aW9uEiYuZW5ncmFtLmFwcC52MS5VcGRhdGVDb25uZWN0aW9uUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuVXBkYXRlQ29ubmVjdGlvblJlc3BvbnNlEmMKEERlbGV0ZUNvbm5lY3Rpb24SJi5lbmdyYW0uYXBwLnYxLkRlbGV0ZUNvbm5lY3Rpb25SZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5EZWxldGVDb25uZWN0aW9uUmVzcG9uc2USXQoOVGVzdENvbm5lY3Rpb24SJC5lbmdyYW0uYXBwLnYxLlRlc3RDb25uZWN0aW9uUmVxdWVzdBolLmVuZ3JhbS5hcHAudjEuVGVzdENvbm5lY3Rpb25SZXNwb25zZRJvChRTZXRDb25uZWN0aW9uRW5hYmxlZBIqLmVuZ3JhbS5hcHAudjEuU2V0Q29ubmVjdGlvbkVuYWJsZWRSZXF1ZXN0GisuZW5ncmFtLmFwcC52MS5TZXRDb25uZWN0aW9uRW5hYmxlZFJlc3BvbnNlEmwKE0dldEdvb2dsZUNsb3VkU2V0dXASKS5lbmdyYW0uYXBwLnYxLkdldEdvb2dsZUNsb3VkU2V0dXBSZXF1ZXN0GiouZW5ncmFtLmFwcC52MS5HZXRHb29nbGVDbG91ZFNldHVwUmVzcG9uc2USXQoOR2V0TWNwQ2F0YWxvZ3MSJC5lbmdyYW0uYXBwLnYxLkdldE1jcENhdGFsb2dzUmVxdWVzdBolLmVuZ3JhbS5hcHAudjEuR2V0TWNwQ2F0YWxvZ3NSZXNwb25zZRJUCgtEaXNjb3Zlck1jcBIhLmVuZ3JhbS5hcHAudjEuRGlzY292ZXJNY3BSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5EaXNjb3Zlck1jcFJlc3BvbnNlElEKClB1Ymxpc2hNY3ASIC5lbmdyYW0uYXBwLnYxLlB1Ymxpc2hNY3BSZXF1ZXN0GiEuZW5ncmFtLmFwcC52MS5QdWJsaXNoTWNwUmVzcG9uc2USYwoQUHV0TWNwQ3JlZGVudGlhbBImLmVuZ3JhbS5hcHAudjEuUHV0TWNwQ3JlZGVudGlhbFJlcXVlc3QaJy5lbmdyYW0uYXBwLnYxLlB1dE1jcENyZWRlbnRpYWxSZXNwb25zZRJUCgtHZXRNY3BTZXR1cBIhLmVuZ3JhbS5hcHAudjEuR2V0TWNwU2V0dXBSZXF1ZXN0GiIuZW5ncmFtLmFwcC52MS5HZXRNY3BTZXR1cFJlc3BvbnNlEnsKGENvbXBsZXRlTWNwQXV0aG9yaXphdGlvbhIuLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVxdWVzdBovLmVuZ3JhbS5hcHAudjEuQ29tcGxldGVNY3BBdXRob3JpemF0aW9uUmVzcG9uc2USbwoUUmVjb3Zlck1jcENyZWRlbnRpYWwSKi5lbmdyYW0uYXBwLnYxLlJlY292ZXJNY3BDcmVkZW50aWFsUmVxdWVzdBorLmVuZ3JhbS5hcHAudjEuUmVjb3Zlck1jcENyZWRlbnRpYWxSZXNwb25zZRJXCgxBdXRob3JpemVNY3ASIi5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlcXVlc3QaIy5lbmdyYW0uYXBwLnYxLkF1dGhvcml6ZU1jcFJlc3BvbnNlEloKDURpc2Nvbm5lY3RNY3ASIy5lbmdyYW0uYXBwLnYxLkRpc2Nvbm5lY3RNY3BSZXF1ZXN0GiQuZW5ncmFtLmFwcC52MS5EaXNjb25uZWN0TWNwUmVzcG9uc2USaQoSR2V0U2Vzc2lvbk1jcFN0YXRlEiguZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXF1ZXN0GikuZW5ncmFtLmFwcC52MS5HZXRTZXNzaW9uTWNwU3RhdGVSZXNwb25zZRJjChBMaXN0TWNwQXBwcm92YWxzEiYuZW5ncmFtLmFwcC52MS5MaXN0TWNwQXBwcm92YWxzUmVxdWVzdBonLmVuZ3JhbS5hcHAudjEuTGlzdE1jcEFwcHJvdmFsc1Jlc3BvbnNlEmYKEURlY2lkZU1jcEFwcHJvdmFsEicuZW5ncmFtLmFwcC52MS5EZWNpZGVNY3BBcHByb3ZhbFJlcXVlc3QaKC5lbmdyYW0uYXBwLnYxLkRlY2lkZU1jcEFwcHJvdmFsUmVzcG9uc2USWgoNQ2FuY2VsTWNwQ2FsbBIjLmVuZ3JhbS5hcHAudjEuQ2FuY2VsTWNwQ2FsbFJlcXVlc3QaJC5lbmdyYW0uYXBwLnYxLkNhbmNlbE1jcENhbGxSZXNwb25zZRJmChFSZXNvbHZlTWNwT3V0Y29tZRInLmVuZ3JhbS5hcHAudjEuUmVzb2x2ZU1jcE91dGNvbWVSZXF1ZXN0GiguZW5ncmFtLmFwcC52MS5SZXNvbHZlTWNwT3V0Y29tZVJlc3BvbnNlEmMKEFByZXBhcmVNY3BSZWJpbmQSJi5lbmdyYW0uYXBwLnYxLlByZXBhcmVNY3BSZWJpbmRSZXF1ZXN0GicuZW5ncmFtLmFwcC52MS5QcmVwYXJlTWNwUmViaW5kUmVzcG9uc2USTgoJUmViaW5kTWNwEh8uZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXF1ZXN0GiAuZW5ncmFtLmFwcC52MS5SZWJpbmRNY3BSZXNwb25zZWIGcHJvdG8z", [file_engram_app_v1_session]); /** * @generated from message engram.app.v1.GoogleCloudConnectionConfig @@ -1246,6 +1246,55 @@ export type McpCatalog = Message<"engram.app.v1.McpCatalog"> & { export const McpCatalogSchema: GenMessage = /*@__PURE__*/ messageDesc(file_engram_app_v1_integration, 40); +/** + * @generated from message engram.app.v1.GetMcpCatalogsRequest + */ +export type GetMcpCatalogsRequest = Message<"engram.app.v1.GetMcpCatalogsRequest"> & { + /** + * @generated from field: string connection_id = 1; + */ + connectionId: string; +}; + +/** + * Describes the message engram.app.v1.GetMcpCatalogsRequest. + * Use `create(GetMcpCatalogsRequestSchema)` to create a new message. + */ +export const GetMcpCatalogsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 41); + +/** + * @generated from message engram.app.v1.GetMcpCatalogsResponse + */ +export type GetMcpCatalogsResponse = Message<"engram.app.v1.GetMcpCatalogsResponse"> & { + /** + * @generated from field: uint32 config_revision = 1; + */ + configRevision: number; + + /** + * @generated from field: uint32 catalog_revision = 2; + */ + catalogRevision: number; + + /** + * @generated from field: engram.app.v1.McpCatalog draft = 3; + */ + draft?: McpCatalog; + + /** + * @generated from field: engram.app.v1.McpCatalog published = 4; + */ + published?: McpCatalog; +}; + +/** + * Describes the message engram.app.v1.GetMcpCatalogsResponse. + * Use `create(GetMcpCatalogsResponseSchema)` to create a new message. + */ +export const GetMcpCatalogsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_engram_app_v1_integration, 42); + /** * @generated from message engram.app.v1.DiscoverMcpRequest */ @@ -1266,7 +1315,7 @@ export type DiscoverMcpRequest = Message<"engram.app.v1.DiscoverMcpRequest"> & { * Use `create(DiscoverMcpRequestSchema)` to create a new message. */ export const DiscoverMcpRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 41); + messageDesc(file_engram_app_v1_integration, 43); /** * @generated from message engram.app.v1.DiscoverMcpResponse @@ -1283,7 +1332,7 @@ export type DiscoverMcpResponse = Message<"engram.app.v1.DiscoverMcpResponse"> & * Use `create(DiscoverMcpResponseSchema)` to create a new message. */ export const DiscoverMcpResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 42); + messageDesc(file_engram_app_v1_integration, 44); /** * @generated from message engram.app.v1.PublishMcpRequest @@ -1315,7 +1364,7 @@ export type PublishMcpRequest = Message<"engram.app.v1.PublishMcpRequest"> & { * Use `create(PublishMcpRequestSchema)` to create a new message. */ export const PublishMcpRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 43); + messageDesc(file_engram_app_v1_integration, 45); /** * @generated from message engram.app.v1.PublishMcpResponse @@ -1332,7 +1381,7 @@ export type PublishMcpResponse = Message<"engram.app.v1.PublishMcpResponse"> & { * Use `create(PublishMcpResponseSchema)` to create a new message. */ export const PublishMcpResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 44); + messageDesc(file_engram_app_v1_integration, 46); /** * @generated from message engram.app.v1.AuthorizeMcpRequest @@ -1364,7 +1413,7 @@ export type AuthorizeMcpRequest = Message<"engram.app.v1.AuthorizeMcpRequest"> & * Use `create(AuthorizeMcpRequestSchema)` to create a new message. */ export const AuthorizeMcpRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 45); + messageDesc(file_engram_app_v1_integration, 47); /** * @generated from message engram.app.v1.AuthorizeMcpResponse @@ -1401,7 +1450,7 @@ export type AuthorizeMcpResponse = Message<"engram.app.v1.AuthorizeMcpResponse"> * Use `create(AuthorizeMcpResponseSchema)` to create a new message. */ export const AuthorizeMcpResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 46); + messageDesc(file_engram_app_v1_integration, 48); /** * @generated from message engram.app.v1.DisconnectMcpRequest @@ -1428,7 +1477,7 @@ export type DisconnectMcpRequest = Message<"engram.app.v1.DisconnectMcpRequest"> * Use `create(DisconnectMcpRequestSchema)` to create a new message. */ export const DisconnectMcpRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 47); + messageDesc(file_engram_app_v1_integration, 49); /** * @generated from message engram.app.v1.DisconnectMcpResponse @@ -1445,7 +1494,7 @@ export type DisconnectMcpResponse = Message<"engram.app.v1.DisconnectMcpResponse * Use `create(DisconnectMcpResponseSchema)` to create a new message. */ export const DisconnectMcpResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 48); + messageDesc(file_engram_app_v1_integration, 50); /** * @generated from message engram.app.v1.McpApproval @@ -1522,7 +1571,7 @@ export type McpApproval = Message<"engram.app.v1.McpApproval"> & { * Use `create(McpApprovalSchema)` to create a new message. */ export const McpApprovalSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 49); + messageDesc(file_engram_app_v1_integration, 51); /** * @generated from message engram.app.v1.ListMcpApprovalsRequest @@ -1544,7 +1593,7 @@ export type ListMcpApprovalsRequest = Message<"engram.app.v1.ListMcpApprovalsReq * Use `create(ListMcpApprovalsRequestSchema)` to create a new message. */ export const ListMcpApprovalsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 50); + messageDesc(file_engram_app_v1_integration, 52); /** * @generated from message engram.app.v1.ListMcpApprovalsResponse @@ -1566,7 +1615,7 @@ export type ListMcpApprovalsResponse = Message<"engram.app.v1.ListMcpApprovalsRe * Use `create(ListMcpApprovalsResponseSchema)` to create a new message. */ export const ListMcpApprovalsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 51); + messageDesc(file_engram_app_v1_integration, 53); /** * @generated from message engram.app.v1.DecideMcpApprovalRequest @@ -1593,7 +1642,7 @@ export type DecideMcpApprovalRequest = Message<"engram.app.v1.DecideMcpApprovalR * Use `create(DecideMcpApprovalRequestSchema)` to create a new message. */ export const DecideMcpApprovalRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 52); + messageDesc(file_engram_app_v1_integration, 54); /** * @generated from message engram.app.v1.DecideMcpApprovalResponse @@ -1615,7 +1664,7 @@ export type DecideMcpApprovalResponse = Message<"engram.app.v1.DecideMcpApproval * Use `create(DecideMcpApprovalResponseSchema)` to create a new message. */ export const DecideMcpApprovalResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 53); + messageDesc(file_engram_app_v1_integration, 55); /** * @generated from message engram.app.v1.CancelMcpCallRequest @@ -1637,7 +1686,7 @@ export type CancelMcpCallRequest = Message<"engram.app.v1.CancelMcpCallRequest"> * Use `create(CancelMcpCallRequestSchema)` to create a new message. */ export const CancelMcpCallRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 54); + messageDesc(file_engram_app_v1_integration, 56); /** * @generated from message engram.app.v1.CancelMcpCallResponse @@ -1659,7 +1708,7 @@ export type CancelMcpCallResponse = Message<"engram.app.v1.CancelMcpCallResponse * Use `create(CancelMcpCallResponseSchema)` to create a new message. */ export const CancelMcpCallResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 55); + messageDesc(file_engram_app_v1_integration, 57); /** * @generated from message engram.app.v1.ResolveMcpOutcomeRequest @@ -1686,7 +1735,7 @@ export type ResolveMcpOutcomeRequest = Message<"engram.app.v1.ResolveMcpOutcomeR * Use `create(ResolveMcpOutcomeRequestSchema)` to create a new message. */ export const ResolveMcpOutcomeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 56); + messageDesc(file_engram_app_v1_integration, 58); /** * @generated from message engram.app.v1.ResolveMcpOutcomeResponse @@ -1708,7 +1757,7 @@ export type ResolveMcpOutcomeResponse = Message<"engram.app.v1.ResolveMcpOutcome * Use `create(ResolveMcpOutcomeResponseSchema)` to create a new message. */ export const ResolveMcpOutcomeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 57); + messageDesc(file_engram_app_v1_integration, 59); /** * @generated from message engram.app.v1.PrepareMcpRebindRequest @@ -1730,7 +1779,7 @@ export type PrepareMcpRebindRequest = Message<"engram.app.v1.PrepareMcpRebindReq * Use `create(PrepareMcpRebindRequestSchema)` to create a new message. */ export const PrepareMcpRebindRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 58); + messageDesc(file_engram_app_v1_integration, 60); /** * @generated from message engram.app.v1.PrepareMcpRebindResponse @@ -1782,7 +1831,7 @@ export type PrepareMcpRebindResponse = Message<"engram.app.v1.PrepareMcpRebindRe * Use `create(PrepareMcpRebindResponseSchema)` to create a new message. */ export const PrepareMcpRebindResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 59); + messageDesc(file_engram_app_v1_integration, 61); /** * @generated from message engram.app.v1.McpRebindDrain @@ -1804,7 +1853,7 @@ export type McpRebindDrain = Message<"engram.app.v1.McpRebindDrain"> & { * Use `create(McpRebindDrainSchema)` to create a new message. */ export const McpRebindDrainSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 60); + messageDesc(file_engram_app_v1_integration, 62); /** * @generated from message engram.app.v1.McpRebindConnection @@ -1856,7 +1905,7 @@ export type McpRebindConnection = Message<"engram.app.v1.McpRebindConnection"> & * Use `create(McpRebindConnectionSchema)` to create a new message. */ export const McpRebindConnectionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 61); + messageDesc(file_engram_app_v1_integration, 63); /** * @generated from message engram.app.v1.RebindMcpRequest @@ -1883,7 +1932,7 @@ export type RebindMcpRequest = Message<"engram.app.v1.RebindMcpRequest"> & { * Use `create(RebindMcpRequestSchema)` to create a new message. */ export const RebindMcpRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 62); + messageDesc(file_engram_app_v1_integration, 64); /** * @generated from message engram.app.v1.RebindMcpResponse @@ -1900,7 +1949,7 @@ export type RebindMcpResponse = Message<"engram.app.v1.RebindMcpResponse"> & { * Use `create(RebindMcpResponseSchema)` to create a new message. */ export const RebindMcpResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 63); + messageDesc(file_engram_app_v1_integration, 65); /** * Entered values are request-only; responses contain metadata and references. @@ -1944,7 +1993,7 @@ export type PutMcpCredentialRequest = Message<"engram.app.v1.PutMcpCredentialReq * Use `create(PutMcpCredentialRequestSchema)` to create a new message. */ export const PutMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 64); + messageDesc(file_engram_app_v1_integration, 66); /** * @generated from message engram.app.v1.McpCredentialStatus @@ -1986,7 +2035,7 @@ export type McpCredentialStatus = Message<"engram.app.v1.McpCredentialStatus"> & * Use `create(McpCredentialStatusSchema)` to create a new message. */ export const McpCredentialStatusSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 65); + messageDesc(file_engram_app_v1_integration, 67); /** * @generated from message engram.app.v1.PutMcpCredentialResponse @@ -2008,7 +2057,7 @@ export type PutMcpCredentialResponse = Message<"engram.app.v1.PutMcpCredentialRe * Use `create(PutMcpCredentialResponseSchema)` to create a new message. */ export const PutMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 66); + messageDesc(file_engram_app_v1_integration, 68); /** * @generated from message engram.app.v1.CompleteMcpAuthorizationResponse @@ -2030,7 +2079,7 @@ export type CompleteMcpAuthorizationResponse = Message<"engram.app.v1.CompleteMc * Use `create(CompleteMcpAuthorizationResponseSchema)` to create a new message. */ export const CompleteMcpAuthorizationResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 67); + messageDesc(file_engram_app_v1_integration, 69); /** * @generated from message engram.app.v1.RecoverMcpCredentialResponse @@ -2052,7 +2101,7 @@ export type RecoverMcpCredentialResponse = Message<"engram.app.v1.RecoverMcpCred * Use `create(RecoverMcpCredentialResponseSchema)` to create a new message. */ export const RecoverMcpCredentialResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 68); + messageDesc(file_engram_app_v1_integration, 70); /** * @generated from message engram.app.v1.McpCredentialState @@ -2074,7 +2123,7 @@ export type McpCredentialState = Message<"engram.app.v1.McpCredentialState"> & { * Use `create(McpCredentialStateSchema)` to create a new message. */ export const McpCredentialStateSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 69); + messageDesc(file_engram_app_v1_integration, 71); /** * @generated from message engram.app.v1.McpApprovedDestination @@ -2096,7 +2145,7 @@ export type McpApprovedDestination = Message<"engram.app.v1.McpApprovedDestinati * Use `create(McpApprovedDestinationSchema)` to create a new message. */ export const McpApprovedDestinationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 70); + messageDesc(file_engram_app_v1_integration, 72); /** * @generated from message engram.app.v1.McpClientRegistration @@ -2143,7 +2192,7 @@ export type McpClientRegistration = Message<"engram.app.v1.McpClientRegistration * Use `create(McpClientRegistrationSchema)` to create a new message. */ export const McpClientRegistrationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 71); + messageDesc(file_engram_app_v1_integration, 73); /** * @generated from message engram.app.v1.GetMcpSetupRequest @@ -2160,7 +2209,7 @@ export type GetMcpSetupRequest = Message<"engram.app.v1.GetMcpSetupRequest"> & { * Use `create(GetMcpSetupRequestSchema)` to create a new message. */ export const GetMcpSetupRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 72); + messageDesc(file_engram_app_v1_integration, 74); /** * @generated from message engram.app.v1.GetMcpSetupResponse @@ -2205,6 +2254,11 @@ export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & * @generated from field: bool client_secret_configured = 8; */ clientSecretConfigured: boolean; + + /** + * @generated from field: bool discovery_available = 9; + */ + discoveryAvailable: boolean; }; /** @@ -2212,7 +2266,7 @@ export type GetMcpSetupResponse = Message<"engram.app.v1.GetMcpSetupResponse"> & * Use `create(GetMcpSetupResponseSchema)` to create a new message. */ export const GetMcpSetupResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 73); + messageDesc(file_engram_app_v1_integration, 75); /** * @generated from message engram.app.v1.CompleteMcpAuthorizationRequest @@ -2264,7 +2318,7 @@ export type CompleteMcpAuthorizationRequest = Message<"engram.app.v1.CompleteMcp * Use `create(CompleteMcpAuthorizationRequestSchema)` to create a new message. */ export const CompleteMcpAuthorizationRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 74); + messageDesc(file_engram_app_v1_integration, 76); /** * @generated from message engram.app.v1.RecoverMcpCredentialRequest @@ -2291,7 +2345,7 @@ export type RecoverMcpCredentialRequest = Message<"engram.app.v1.RecoverMcpCrede * Use `create(RecoverMcpCredentialRequestSchema)` to create a new message. */ export const RecoverMcpCredentialRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 75); + messageDesc(file_engram_app_v1_integration, 77); /** * Exact server review. Clients must return it unchanged when they decide. @@ -2405,7 +2459,7 @@ export type McpApprovalReviewContext = Message<"engram.app.v1.McpApprovalReviewC * Use `create(McpApprovalReviewContextSchema)` to create a new message. */ export const McpApprovalReviewContextSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 76); + messageDesc(file_engram_app_v1_integration, 78); /** * @generated from message engram.app.v1.McpApprovalReceipt @@ -2447,7 +2501,7 @@ export type McpApprovalReceipt = Message<"engram.app.v1.McpApprovalReceipt"> & { * Use `create(McpApprovalReceiptSchema)` to create a new message. */ export const McpApprovalReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 77); + messageDesc(file_engram_app_v1_integration, 79); /** * @generated from message engram.app.v1.McpExecutionReview @@ -2554,7 +2608,7 @@ export type McpExecutionReview = Message<"engram.app.v1.McpExecutionReview"> & { * Use `create(McpExecutionReviewSchema)` to create a new message. */ export const McpExecutionReviewSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 78); + messageDesc(file_engram_app_v1_integration, 80); /** * @generated from message engram.app.v1.McpUnknownContext @@ -2591,7 +2645,7 @@ export type McpUnknownContext = Message<"engram.app.v1.McpUnknownContext"> & { * Use `create(McpUnknownContextSchema)` to create a new message. */ export const McpUnknownContextSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 79); + messageDesc(file_engram_app_v1_integration, 81); /** * @generated from message engram.app.v1.McpCancellationReceipt @@ -2638,7 +2692,7 @@ export type McpCancellationReceipt = Message<"engram.app.v1.McpCancellationRecei * Use `create(McpCancellationReceiptSchema)` to create a new message. */ export const McpCancellationReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 80); + messageDesc(file_engram_app_v1_integration, 82); /** * @generated from message engram.app.v1.McpResolutionReceipt @@ -2675,7 +2729,7 @@ export type McpResolutionReceipt = Message<"engram.app.v1.McpResolutionReceipt"> * Use `create(McpResolutionReceiptSchema)` to create a new message. */ export const McpResolutionReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 81); + messageDesc(file_engram_app_v1_integration, 83); /** * @generated from message engram.app.v1.McpBindingReceipt @@ -2712,7 +2766,7 @@ export type McpBindingReceipt = Message<"engram.app.v1.McpBindingReceipt"> & { * Use `create(McpBindingReceiptSchema)` to create a new message. */ export const McpBindingReceiptSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 82); + messageDesc(file_engram_app_v1_integration, 84); /** * @generated from message engram.app.v1.GetSessionMcpStateRequest @@ -2764,7 +2818,7 @@ export type GetSessionMcpStateRequest = Message<"engram.app.v1.GetSessionMcpStat * Use `create(GetSessionMcpStateRequestSchema)` to create a new message. */ export const GetSessionMcpStateRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 83); + messageDesc(file_engram_app_v1_integration, 85); /** * @generated from message engram.app.v1.GetSessionMcpStateResponse @@ -2841,7 +2895,7 @@ export type GetSessionMcpStateResponse = Message<"engram.app.v1.GetSessionMcpSta * Use `create(GetSessionMcpStateResponseSchema)` to create a new message. */ export const GetSessionMcpStateResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 84); + messageDesc(file_engram_app_v1_integration, 86); /** * Immutable session grant. Schemas remain independent documents with their own roots. @@ -2910,7 +2964,7 @@ export type McpBoundToolSummary = Message<"engram.app.v1.McpBoundToolSummary"> & * Use `create(McpBoundToolSummarySchema)` to create a new message. */ export const McpBoundToolSummarySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_engram_app_v1_integration, 85); + messageDesc(file_engram_app_v1_integration, 87); /** * @generated from enum engram.app.v1.McpRebindDrainReason @@ -3111,6 +3165,14 @@ export const IntegrationService: GenService<{ input: typeof GetGoogleCloudSetupRequestSchema; output: typeof GetGoogleCloudSetupResponseSchema; }, + /** + * @generated from rpc engram.app.v1.IntegrationService.GetMcpCatalogs + */ + getMcpCatalogs: { + methodKind: "unary"; + input: typeof GetMcpCatalogsRequestSchema; + output: typeof GetMcpCatalogsResponseSchema; + }, /** * @generated from rpc engram.app.v1.IntegrationService.DiscoverMcp */ diff --git a/orchestrator/src/mcp/contracts.ts b/orchestrator/src/mcp/contracts.ts index 32080ea08..5f26f8e1b 100644 --- a/orchestrator/src/mcp/contracts.ts +++ b/orchestrator/src/mcp/contracts.ts @@ -7,7 +7,7 @@ import { } from "../gen/engram/app/v1/integration_pb.ts"; import { McpPublicationRequired, McpRebindDraining, type McpRebindDrain } from "./rebind.ts"; import { policyReviewCurrent, type JournalReviewContext } from "./journal-review.ts"; -import { credentialHandlers } from "./credential-rpc.ts"; +import { credentialHandlers, requireMcpHumanAdmin } from "./credential-rpc.ts"; import type { CredentialAdmin } from "./credential-admin.ts"; import { Code, ConnectError, type ServiceImpl } from "@connectrpc/connect"; import type { IntegrationService } from "../gen/engram/app/v1/integration_pb.ts"; @@ -20,6 +20,8 @@ import { validateMcpConfig, type McpConfig } from "./config.ts"; import type { McpRepository } from "./repository.ts"; import type { McpJournal, McpUnknownContext } from "./journal.ts"; import type { McpDiscoveredTool, McpReviewedTool } from "./types.ts"; +import { assertSchemaShape } from "./catalog.ts"; +import { MCP_LIMITS } from "./limits.ts"; /** Internal services use trusted connection identities, never guest destinations. */ export interface McpServices { @@ -146,8 +148,26 @@ export function mcpConfigToProto(raw: unknown): McpProtoConfig { }; } function catalogToProto(value: Awaited>) { + if ( + !Array.isArray(value.tools) || + value.tools.length > MCP_LIMITS.toolsPerSession || + Buffer.byteLength(JSON.stringify(value.tools)) > MCP_LIMITS.manifestBytes + ) + throw new Error("catalog is too large"); + for (const tool of value.tools) { + if (typeof tool.upstreamName !== "string" || typeof tool.description !== "string") + throw new Error("invalid catalog"); + assertSchemaShape(tool.inputSchema); + if (tool.outputSchema !== undefined) assertSchemaShape(tool.outputSchema); + } return { - ...value, + id: value.id, + connectionId: value.connectionId, + kind: value.kind, + revision: value.revision, + configRevision: value.configRevision, + authGeneration: value.authGeneration, + fingerprint: value.fingerprint, tools: value.tools.map((t) => ({ upstreamName: t.upstreamName, description: t.description, @@ -173,6 +193,7 @@ type Methods = | "getMcpSetup" | "completeMcpAuthorization" | "recoverMcpCredential" + | "getMcpCatalogs" | "discoverMcp" | "publishMcp" | "authorizeMcp" @@ -200,6 +221,26 @@ export function mcpHandlers(deps: { } return { ...credentialHandlers(deps), + async getMcpCatalogs(req, ctx) { + await requireMcpHumanAdmin(ctx, deps.getSession); + const repository = requireMcpHook(deps.services?.repository); + return mcpOperation(async () => { + const row = await connection(req.connectionId); + const [draft, published] = await Promise.all([ + repository.getLatestDraft(row.id), + row.mcp!.catalogRevision ? repository.getCatalog(row.id, row.mcp!.catalogRevision) : null, + ]); + const result = { + configRevision: row.mcp!.configRevision, + catalogRevision: row.mcp!.catalogRevision, + ...(draft ? { draft: catalogToProto(draft) } : {}), + ...(published ? { published: catalogToProto(published) } : {}), + }; + if (Buffer.byteLength(JSON.stringify(result)) > MCP_LIMITS.responseBytes) + throw new Error("catalog response is too large"); + return result; + }); + }, async discoverMcp(req, ctx) { const actor = await requireAdmin(ctx, deps.getSession); const service = services(); diff --git a/orchestrator/src/mcp/credential-rpc.ts b/orchestrator/src/mcp/credential-rpc.ts index 3eb0579d3..ba44b210d 100644 --- a/orchestrator/src/mcp/credential-rpc.ts +++ b/orchestrator/src/mcp/credential-rpc.ts @@ -148,6 +148,7 @@ export function credentialHandlers(deps: { return mcpOperation(async () => { const base = { enabled: service?.enabled ?? false, + discoveryAvailable: !!service?.enabled && !!service.discovery, registrations: service?.credentials?.registrations() ?? [], destinations: service?.destinations ?? [], }; diff --git a/orchestrator/src/mcp/repository.ts b/orchestrator/src/mcp/repository.ts index a430bdc5f..a3a00231b 100644 --- a/orchestrator/src/mcp/repository.ts +++ b/orchestrator/src/mcp/repository.ts @@ -291,13 +291,14 @@ export function makeMcpRepository(db = getDb()) { )[0] ?? null ); }, - async listCatalogs(id: string) { - return db + async getLatestDraft(id: string) { + const [draft] = await db .select() .from(catalog) - .where(eq(catalog.connectionId, id)) - .orderBy(desc(catalog.createdAt)) - .limit(100); + .where(and(eq(catalog.connectionId, id), eq(catalog.kind, "draft"))) + .orderBy(desc(catalog.createdAt), desc(catalog.id)) + .limit(1); + return draft ?? null; }, }; } diff --git a/web/index.html b/web/index.html index 24d87d52b..bb0b5642f 100644 --- a/web/index.html +++ b/web/index.html @@ -2,6 +2,7 @@ + diff --git a/web/src/App.tsx b/web/src/App.tsx index d23291219..64061591d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -21,7 +21,8 @@ const queryClient = new QueryClient({ // useQuery/useMutation connect-query hooks without threading a transport prop everywhere. const transport = createConnectTransport({ baseUrl: RPC_BASE, - fetch: (input, init) => fetch(input, { ...init, credentials: API_CREDENTIALS }), + fetch: (input, init) => + fetch(input, { ...init, credentials: API_CREDENTIALS, cache: "no-store" }), }); function InnerApp() { diff --git a/web/src/app-entry.tsx b/web/src/app-entry.tsx new file mode 100644 index 000000000..cc6df679a --- /dev/null +++ b/web/src/app-entry.tsx @@ -0,0 +1,28 @@ +// JetBrains Mono is a multi-axis variable font; Fontsource splits it into one +// CSS file per axis. Import the `wght` axis so the full weight range is +// available to anything using `--font-mono` (code, IDs, tabular numbers). +import "@fontsource-variable/jetbrains-mono/wght.css"; +import "@fontsource-variable/jetbrains-mono/wght-italic.css"; +// Saira carries page titles only (`--font-display`, set 8% wide). The `wdth` +// file ships the weight AND width axes; the sans body face is system-ui, so +// there is nothing to import for it. +import "@fontsource-variable/saira/wdth.css"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import { ThemeProvider } from "./components/theme-provider"; +// index.css pulls in Tailwind once and @imports theme.css (the Lab-Notebook +// partial kept for the not-yet-migrated SessionDetail transcript subtree). +import "./index.css"; + +// StrictMode intentionally double-mounts effects in dev. Useful in +// general, but it interacts badly with ghostty-web's WASM Terminal: +// the first mount opens a WS to ttyd, cleanup closes it, the second +// mount opens a fresh WS — bash sees two SIGWINCH-driven resize +// flurries on top of each other and the rendered output overlaps. +// Until we make TerminalPane fully StrictMode-idempotent, opt out +// at the root. +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/web/src/components/integrations/GoogleCloudSetupPage.tsx b/web/src/components/integrations/GoogleCloudSetupPage.tsx index 8232ed1a4..2317ae84f 100644 --- a/web/src/components/integrations/GoogleCloudSetupPage.tsx +++ b/web/src/components/integrations/GoogleCloudSetupPage.tsx @@ -34,7 +34,18 @@ import { GoogleCloudEndpointDialog } from "./GoogleCloudConnections"; type SetupCodeLanguage = "terraform" | "shellscript"; export function GoogleCloudSetupPage() { - const { connectionId } = useParams({ strict: false }) as { connectionId?: string }; + const { connectionId, provider } = useParams({ strict: false }) as { + connectionId?: string; + provider?: string; + }; + if (provider === "mcp") + return ( +
+ +
+ ); return ; } diff --git a/web/src/components/integrations/IntegrationDetail.tsx b/web/src/components/integrations/IntegrationDetail.tsx index fa1b3dbfe..f967c47f4 100644 --- a/web/src/components/integrations/IntegrationDetail.tsx +++ b/web/src/components/integrations/IntegrationDetail.tsx @@ -70,6 +70,14 @@ export function IntegrationDetail() { window.history.replaceState({}, "", window.location.pathname); }, []); + if (provider === "mcp") { + return ( +
+ +

Manage external MCP connections in Integrations.

+
+ ); + } if (isLoading) return ; if (!view) { return ( diff --git a/web/src/components/integrations/McpCatalogReview.test.tsx b/web/src/components/integrations/McpCatalogReview.test.tsx new file mode 100644 index 000000000..1174dd7aa --- /dev/null +++ b/web/src/components/integrations/McpCatalogReview.test.tsx @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Code, ConnectError, createRouterTransport } from "@connectrpc/connect"; +import { renderWithProviders } from "@/test-utils"; +import { IntegrationService } from "@/gen/engram/app/v1/integration_pb"; +import { McpCatalogReview } from "./McpCatalogReview"; + +describe("MCP catalog review", () => { + it("reloads the stored draft, renders descriptions as text, and requires explicit classification", async () => { + const published: unknown[] = []; + const transport = createRouterTransport((router) => + router.service(IntegrationService, { + getMcpCatalogs: () => ({ + configRevision: 1, + catalogRevision: 2, + draft: { + id: "draft", + connectionId: "c1", + kind: "draft", + revision: 0, + configRevision: 1, + authGeneration: 1, + tools: [ + { + upstreamName: "create.item", + description: '', + inputSchemaJson: '{"type":"object"}', + }, + ], + }, + }), + publishMcp: (req) => { + published.push(req); + return {}; + }, + }), + ); + renderWithProviders( + , + { transport }, + ); + const user = userEvent.setup(); + await user.click(await screen.findByRole("button", { name: "Review draft" })); + expect(screen.getByText('')).toBeTruthy(); + expect(document.querySelector('img[src="https://example.test/tracker"]')).toBeNull(); + expect( + (screen.getByRole("button", { name: "Publish reviewed catalog" }) as HTMLButtonElement) + .disabled, + ).toBe(true); + await user.selectOptions(screen.getByLabelText("Classification for create.item"), "write"); + await user.click(screen.getByRole("button", { name: "Publish reviewed catalog" })); + await waitFor(() => expect(published).toHaveLength(1)); + expect(published[0]).toMatchObject({ + connectionId: "c1", + draftId: "draft", + expectedRevision: 2, + tools: [{ upstreamName: "create.item", classification: "write", enabled: false }], + }); + }); + it("drops a stale review and requires a new review after a publication conflict", async () => { + let reads = 0; + const transport = createRouterTransport((router) => + router.service(IntegrationService, { + getMcpCatalogs: () => { + reads++; + return { + configRevision: 1, + catalogRevision: reads, + draft: { + id: "draft", + configRevision: 1, + authGeneration: 1, + tools: [{ upstreamName: "read.item", inputSchemaJson: '{"type":"object"}' }], + }, + }; + }, + publishMcp: () => { + throw new ConnectError("stale", Code.Aborted); + }, + }), + ); + renderWithProviders( + , + { transport }, + ); + const user = userEvent.setup(); + await user.click(await screen.findByRole("button", { name: "Review draft" })); + await user.selectOptions(screen.getByLabelText("Classification for read.item"), "read"); + await user.click(screen.getByRole("button", { name: "Publish reviewed catalog" })); + expect(await screen.findByText(/reload and review the draft again/i)).toBeTruthy(); + expect(screen.queryByRole("button", { name: "Publish reviewed catalog" })).toBeNull(); + expect(reads).toBeGreaterThan(1); + }); +}); diff --git a/web/src/components/integrations/McpCatalogReview.tsx b/web/src/components/integrations/McpCatalogReview.tsx new file mode 100644 index 000000000..af379caa7 --- /dev/null +++ b/web/src/components/integrations/McpCatalogReview.tsx @@ -0,0 +1,217 @@ +import { createClient } from "@connectrpc/connect"; +import { useQuery, useTransport } from "@connectrpc/connect-query"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { getMcpCatalogs } from "@/gen/engram/app/v1/integration-IntegrationService_connectquery"; +import { IntegrationService, type McpTool } from "@/gen/engram/app/v1/integration_pb"; + +export function McpCatalogReview({ + connectionId, + revision, + authGeneration, + discoveryAvailable, +}: { + connectionId: string; + revision: number; + authGeneration: number; + discoveryAvailable: boolean; +}) { + const client = createClient(IntegrationService, useTransport()); + const catalogs = useQuery(getMcpCatalogs, { connectionId }); + const [review, setReview] = useState(null); + const [draftId, setDraftId] = useState(""); + const [expectedRevision, setExpectedRevision] = useState(0); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState(""); + async function discover() { + setBusy(true); + setMessage(""); + setReview(null); + try { + await client.discoverMcp({ connectionId, expectedRevision: revision }); + await catalogs.refetch(); + } catch { + setMessage("Discovery could not finish. Reload the connection and try again."); + } finally { + setBusy(false); + } + } + function beginReview() { + if (!catalogs.data?.draft) return; + setDraftId(catalogs.data.draft.id); + setExpectedRevision(catalogs.data.catalogRevision); + setReview( + catalogs.data.draft.tools.map((tool) => ({ ...tool, classification: "", enabled: false })), + ); + setMessage(""); + } + async function publish() { + if ( + !review || + review.some((tool) => tool.classification !== "read" && tool.classification !== "write") + ) + return; + setBusy(true); + setMessage(""); + try { + await client.publishMcp({ connectionId, draftId, expectedRevision, tools: review }); + setReview(null); + await catalogs.refetch(); + setMessage("Catalog published. Review the connection before enabling it."); + } catch { + setReview(null); + await catalogs.refetch(); + setMessage( + "Could not publish. The connection or catalog may have changed. Reload and review the draft again.", + ); + } finally { + setBusy(false); + } + } + const published = catalogs.data?.published; + const draft = catalogs.data?.draft; + const draftNames = new Set(draft?.tools.map((tool) => tool.upstreamName)); + const staleDraft = + !!draft && + (draft.configRevision !== catalogs.data?.configRevision || + draft.authGeneration !== authGeneration); + const previous = new Map(published?.tools.map((tool) => [tool.upstreamName, tool])); + return ( +
+

Tool catalog

+ {catalogs.isError &&

Catalog data is unavailable.

} + {catalogs.data && ( +

+ Published revision {catalogs.data.catalogRevision} · {published?.tools.length ?? 0} tools + · {draft ? "draft available" : "no draft"} +

+ )} + {published && ( +
+ Published tools + {published.tools.map((tool) => ( +

+ {tool.upstreamName} · {tool.classification} ·{" "} + {tool.enabled ? "enabled" : "disabled"} +

+ ))} +
+ )} + {published?.tools + .filter((tool) => draft && !draftNames.has(tool.upstreamName)) + .map((tool) => ( +

+ Removed from draft: {tool.upstreamName} +

+ ))} + {staleDraft && ( +

This draft is stale. Discover the tools again before publishing.

+ )} +
+ + +
+ {review && ( +
+ {review.map((tool, index) => ( +
+ {tool.upstreamName} + + {!previous.has(tool.upstreamName) + ? "New" + : previous.get(tool.upstreamName)?.inputSchemaJson !== tool.inputSchemaJson || + previous.get(tool.upstreamName)?.outputSchemaJson !== tool.outputSchemaJson || + previous.get(tool.upstreamName)?.description !== tool.description + ? "Changed" + : "Unchanged"} + + {previous.has(tool.upstreamName) && ( +
+ Previous review +

+ {previous.get(tool.upstreamName)?.description} +

+
+                    {previous.get(tool.upstreamName)?.inputSchemaJson}
+                  
+
+                    {previous.get(tool.upstreamName)?.outputSchemaJson}
+                  
+

+ {previous.get(tool.upstreamName)?.classification} ·{" "} + {previous.get(tool.upstreamName)?.enabled ? "enabled" : "disabled"} +

+
+ )} +

{tool.description}

+
+ Input schema +
{tool.inputSchemaJson}
+
+ {tool.outputSchemaJson && ( +
+ Output schema +
{tool.outputSchemaJson}
+
+ )} + + +
+ ))} + +
+ )} + {message &&

{message}

} +
+ ); +} diff --git a/web/src/components/integrations/McpConnections.test.tsx b/web/src/components/integrations/McpConnections.test.tsx new file mode 100644 index 000000000..18148c72d --- /dev/null +++ b/web/src/components/integrations/McpConnections.test.tsx @@ -0,0 +1,175 @@ +import { describe, expect, it } from "vitest"; +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { create } from "@bufbuild/protobuf"; +import { createRouterTransport } from "@connectrpc/connect"; +import { renderWithProviders } from "@/test-utils"; +import { GetMcpSetupResponseSchema, IntegrationService } from "@/gen/engram/app/v1/integration_pb"; +import { McpConnections } from "./McpConnections"; +import { MCP_CALLBACK_PATH, saveMcpFlow } from "@/mcp-callback-bootstrap"; + +describe("external MCP connections", () => { + it("offers only approved endpoints and creates disabled metadata", async () => { + const requests: unknown[] = []; + const transport = createRouterTransport((router) => + router.service(IntegrationService, { + listConnections: () => ({ connections: [] }), + createConnection: (req) => { + requests.push(req); + return {}; + }, + }), + ); + renderWithProviders( + , + { transport }, + ); + const user = userEvent.setup(); + await user.type(await screen.findByLabelText("Connection name"), "Tools"); + await user.type(screen.getByLabelText("Connection alias"), "tools"); + await user.click(screen.getByRole("button", { name: "Create connection" })); + await waitFor(() => expect(requests).toHaveLength(1)); + expect(requests[0]).toMatchObject({ + alias: "tools", + provider: "mcp", + mcp: { zone: "public", endpoint: "https://tools.example/mcp", authKind: "api_key" }, + }); + }); + + it("sends an API key directly and clears the input after failure", async () => { + const requests: string[] = []; + const connection = { + id: "c1", + provider: "mcp", + alias: "tools", + displayName: "Tools", + configRevision: 1, + authGeneration: 1, + mcp: { endpoint: "https://tools.example/mcp", zone: "public", authKind: "api_key" }, + }; + const transport = createRouterTransport((router) => + router.service(IntegrationService, { + listConnections: () => ({ connections: [connection] }), + getMcpSetup: () => ({ + enabled: true, + current: { connection, credential: { status: "not_configured" } }, + }), + getMcpCatalogs: () => ({}), + putMcpCredential: (req) => { + requests.push(req.value); + throw new Error("failed"); + }, + }), + ); + const { queryClient } = renderWithProviders( + , + { transport }, + ); + const user = userEvent.setup(); + const input = (await screen.findByLabelText("API key for Tools")) as HTMLInputElement; + await user.type(input, "key-secret"); + await user.click(screen.getByRole("button", { name: "Save key" })); + await waitFor(() => expect(requests).toEqual(["key-secret"])); + await waitFor(() => expect(input.value).toBe("")); + expect(queryClient.getMutationCache().getAll()).toHaveLength(0); + expect(JSON.stringify(sessionStorage)).not.toContain("key-secret"); + }); + it("does not start another OAuth flow while this tab has one", async () => { + const redirectUri = `${window.location.origin}${MCP_CALLBACK_PATH}`; + saveMcpFlow(sessionStorage, { + connectionId: "other", + flowId: "f1", + configRevision: 1, + authGeneration: 1, + redirectUri, + expiresAt: "2099-01-01T00:00:00Z", + }); + const connection = { + id: "c1", + provider: "mcp", + alias: "tools", + displayName: "Tools", + configRevision: 1, + authGeneration: 1, + mcp: { + endpoint: "https://tools.example/mcp", + zone: "public", + authKind: "oauth", + oauthClientRegistrationId: "reg", + shareWithOrgSessions: true, + }, + }; + let calls = 0; + const transport = createRouterTransport((router) => + router.service(IntegrationService, { + listConnections: () => ({ connections: [connection] }), + getMcpSetup: () => ({ enabled: true, current: { connection } }), + getMcpCatalogs: () => ({}), + authorizeMcp: () => { + calls++; + return {}; + }, + }), + ); + try { + renderWithProviders( + , + { transport }, + ); + const user = userEvent.setup(); + await user.click(await screen.findByRole("button", { name: "Authorize" })); + await screen.findByRole("alert"); + expect(calls).toBe(0); + expect(sessionStorage.length).toBe(1); + } finally { + sessionStorage.clear(); + } + }); + it("keeps existing connections visible and marks unavailable actions when disabled", async () => { + const connection = { + id: "c1", + provider: "mcp", + alias: "tools", + displayName: "Tools", + mcp: { endpoint: "https://tools.example/mcp", zone: "public", authKind: "api_key" }, + }; + const transport = createRouterTransport((router) => + router.service(IntegrationService, { + listConnections: () => ({ connections: [connection] }), + getMcpSetup: () => ({ enabled: false, current: { connection } }), + getMcpCatalogs: () => ({}), + }), + ); + renderWithProviders( + , + { transport }, + ); + expect(await screen.findByRole("region", { name: "Tools MCP connection" })).toBeTruthy(); + expect((screen.getByRole("button", { name: "Recover" }) as HTMLButtonElement).disabled).toBe( + true, + ); + expect(screen.getByText(/actions and recovery are unavailable/i)).toBeTruthy(); + }); +}); diff --git a/web/src/components/integrations/McpConnections.tsx b/web/src/components/integrations/McpConnections.tsx new file mode 100644 index 000000000..5aecaf98b --- /dev/null +++ b/web/src/components/integrations/McpConnections.tsx @@ -0,0 +1,613 @@ +import { createClient } from "@connectrpc/connect"; +import { useQuery, useTransport } from "@connectrpc/connect-query"; +import { useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + getMcpSetup, + listConnections, +} from "@/gen/engram/app/v1/integration-IntegrationService_connectquery"; +import { + IntegrationService, + type GetMcpSetupResponse, + type IntegrationConnection, +} from "@/gen/engram/app/v1/integration_pb"; +import { McpCatalogReview } from "./McpCatalogReview"; +import { + MCP_CALLBACK_PATH, + saveMcpFlow, + assertMcpFlowAvailable, + clearMcpFlowForConnection, + clearMcpFlow, +} from "@/mcp-callback-bootstrap"; + +export function McpConnections({ setup }: { setup: GetMcpSetupResponse }) { + const transport = useTransport(); + const client = createClient(IntegrationService, transport); + const connections = useQuery(listConnections, {}); + const [name, setName] = useState(""); + const [alias, setAlias] = useState(""); + const [destination, setDestination] = useState(0); + const [kind, setKind] = useState("api_key"); + const [registration, setRegistration] = useState(""); + const [sharing, setSharing] = useState(false); + const [message, setMessage] = useState(""); + const [busy, setBusy] = useState(false); + const selected = setup.destinations[destination]; + const redirect = `${window.location.origin}${MCP_CALLBACK_PATH}`; + const registrations = setup.registrations.filter( + (item) => + item.redirectUri === redirect && + item.resource?.zoneId === selected?.zoneId && + item.resource?.endpoint === selected?.endpoint, + ); + async function createConnection() { + if ( + !selected || + !name.trim() || + !alias.trim() || + (kind === "oauth" && (!sharing || !registrations.some((r) => r.id === registration))) + ) + return; + setBusy(true); + setMessage(""); + try { + await client.createConnection({ + alias: alias.trim(), + provider: "mcp", + displayName: name.trim(), + mcp: { + endpoint: selected.endpoint, + zone: selected.zoneId, + authKind: kind, + apiKeySecretRef: "", + oauthClientRegistrationId: kind === "oauth" ? registration : "", + shareWithOrgSessions: kind === "oauth" && sharing, + }, + }); + setName(""); + setAlias(""); + await connections.refetch(); + } catch { + setMessage("Could not create the connection. Check the settings and try again."); + } finally { + setBusy(false); + } + } + return ( +
+ {(connections.data?.connections ?? []) + .filter((c) => c.provider === "mcp") + .map((connection) => ( + connections.refetch()} + /> + ))} + {!setup.discoveryAvailable && ( +

Discovery is unavailable.

+ )} + {setup.enabled && setup.destinations.length > 0 && ( + <> +

Add a connection

+ + + + + {kind === "oauth" && ( + <> + + + + )} + + {message &&

{message}

} + + )} +
+ ); +} + +function McpConnection({ + connection, + setup, + refresh, +}: { + connection: IntegrationConnection; + setup: GetMcpSetupResponse; + refresh: () => unknown; +}) { + const client = createClient(IntegrationService, useTransport()); + const status = useQuery(getMcpSetup, { connectionId: connection.id }); + const secret = useRef(null); + const [header, setHeader] = useState("authorization"); + const [editing, setEditing] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); + const [editName, setEditName] = useState(connection.displayName); + const [editAlias, setEditAlias] = useState(connection.alias); + const [editDestination, setEditDestination] = useState(""); + const [editKind, setEditKind] = useState("api_key"); + const [editRegistration, setEditRegistration] = useState(""); + const [editSharing, setEditSharing] = useState(false); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState(""); + const current = status.data?.current?.connection ?? connection; + async function run(action: () => Promise) { + setBusy(true); + setMessage(""); + try { + await action(); + await Promise.all([status.refetch(), refresh()]); + } catch { + setMessage( + "The action could not be completed. Reload the connection and try again or recover it.", + ); + } finally { + if (secret.current) secret.current.value = ""; + setBusy(false); + } + } + const request = { + connectionId: current.id, + expectedRevision: current.configRevision, + expectedGeneration: current.authGeneration, + }; + async function authorize() { + assertMcpFlowAvailable(sessionStorage); + const reg = setup.registrations.find((r) => r.id === current.mcp?.oauthClientRegistrationId); + const redirectUri = `${window.location.origin}${MCP_CALLBACK_PATH}`; + if (!reg || reg.redirectUri !== redirectUri) throw new Error("Callback is not registered"); + const result = await client.authorizeMcp({ ...request, shareWithOrgSessions: true }); + saveMcpFlow(sessionStorage, { + connectionId: current.id, + flowId: result.flowId, + configRevision: result.configRevision, + authGeneration: result.authGeneration, + redirectUri, + expiresAt: result.expiresAt, + }); + window.location.assign(result.authorizationUrl); + } + return ( +
+

{connection.displayName}

+

+ {current.mcp?.zone} · {current.mcp?.endpoint} · {current.enabled ? "Enabled" : "Disabled"} +

+

+ Credential: {status.data?.current?.credential?.status ?? "unavailable"}{" "} + {status.data?.current?.credential?.reference ?? ""} +

+ {!setup.enabled && ( +

+ Connection actions and recovery are unavailable while this service is disabled. +

+ )} + {!editing ? ( + + ) : ( +
+

+ Changing connection settings can require new authorization and catalog review. +

+ + + + + {editKind === "oauth" && ( + <> + + + + )} +
+ + +
+
+ )} + {current.mcp?.authKind === "api_key" && ( + <> + + + + + )} + {current.mcp?.authKind === "oauth" && ( + <> + {setup.registrations.find((item) => item.id === current.mcp?.oauthClientRegistrationId) + ?.confidential && ( + <> +

+ Client secret:{" "} + {status.data?.clientSecretConfigured ? "configured" : "not configured"} +

+ + + + )} + + + )} +
+ + + +
+ {confirmDelete ? ( +
+ + Delete this connection? Profiles that use it must be updated first. + + + +
+ ) : ( + + )} + +
+ If this tab has lost authorization context, recover the connection and clear this tab before + starting again. +
+ + {message &&

{message}

} +
+ ); +} diff --git a/web/src/components/profiles/McpGrantEditor.test.tsx b/web/src/components/profiles/McpGrantEditor.test.tsx new file mode 100644 index 000000000..417d4fcf0 --- /dev/null +++ b/web/src/components/profiles/McpGrantEditor.test.tsx @@ -0,0 +1,107 @@ +import { create } from "@bufbuild/protobuf"; +import { createRouterTransport } from "@connectrpc/connect"; +import { useState } from "react"; +import { expect, it } from "vitest"; +import { fireEvent, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { + IntegrationConnectionSchema, + IntegrationService, +} from "@/gen/engram/app/v1/integration_pb"; +import { renderWithProviders } from "@/test-utils"; +import { McpGrantEditor, type McpProfileGrant } from "./McpGrantEditor"; + +it("requires a constraint or explicit unrestricted choice and keeps documents separate", async () => { + const connection = create(IntegrationConnectionSchema, { + id: "c1", + displayName: "Tools", + provider: "mcp", + configRevision: 1, + authGeneration: 1, + catalogRevision: 1, + }); + const transport = createRouterTransport((router) => + router.service(IntegrationService, { + getMcpCatalogs: () => ({ + catalogRevision: 1, + published: { + id: "p1", + revision: 1, + configRevision: 1, + authGeneration: 1, + tools: [{ upstreamName: "write.item", classification: "write", enabled: true }], + }, + }), + }), + ); + function Fixture() { + const [grants, setGrants] = useState([]); + return ( + <> + {}} + /> + {JSON.stringify(grants)} + + ); + } + renderWithProviders(, { transport }); + const user = userEvent.setup(); + await user.selectOptions(await screen.findByLabelText("Reviewed tool for Tools"), "write.item"); + expect( + (screen.getByRole("button", { name: "Add MCP grant" }) as HTMLButtonElement).disabled, + ).toBe(true); + fireEvent.change(screen.getByLabelText("Argument constraint JSON"), { + target: { value: '{"type":"object"}' }, + }); + await user.click(screen.getByRole("button", { name: "Add MCP grant" })); + await user.click(screen.getByRole("button", { name: "Add constraint document" })); + const output = JSON.parse(screen.getByRole("status").textContent ?? "[]"); + expect(output[0].mcp.argumentConstraintDocumentsJson).toEqual(['{"type":"object"}', ""]); + expect(output[0].mcp.unrestricted).toBe(false); +}); + +it("does not offer tools or publication review for a stale catalog", async () => { + const connection = create(IntegrationConnectionSchema, { + id: "c1", + displayName: "Tools", + provider: "mcp", + configRevision: 2, + authGeneration: 1, + catalogRevision: 1, + }); + const transport = createRouterTransport((router) => + router.service(IntegrationService, { + getMcpCatalogs: () => ({ + catalogRevision: 1, + published: { + id: "p1", + revision: 1, + configRevision: 1, + authGeneration: 1, + tools: [{ upstreamName: "write.item", classification: "write", enabled: true }], + }, + }), + }), + ); + const reports: boolean[] = []; + renderWithProviders( + {}} + onAvailability={(_id, ready) => { + reports.push(ready); + }} + />, + { transport }, + ); + expect((await screen.findByRole("alert")).textContent).toContain( + "current reviewed catalog is unavailable", + ); + expect(screen.queryByLabelText("Reviewed tool for Tools")).toBeNull(); + expect(reports.at(-1)).toBe(false); +}); diff --git a/web/src/components/profiles/McpGrantEditor.tsx b/web/src/components/profiles/McpGrantEditor.tsx new file mode 100644 index 000000000..0a5022884 --- /dev/null +++ b/web/src/components/profiles/McpGrantEditor.tsx @@ -0,0 +1,266 @@ +import { useQuery } from "@connectrpc/connect-query"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { getMcpCatalogs } from "@/gen/engram/app/v1/integration-IntegrationService_connectquery"; +import type { IntegrationConnection } from "@/gen/engram/app/v1/integration_pb"; + +export type McpProfileGrant = { + connectionId: string; + operation: string; + resourceConstraints: string[]; + credentialScope?: string; + mcp?: { + upstreamName: string; + automationWrite: boolean; + unrestricted: boolean; + argumentConstraintDocumentsJson: string[]; + }; +}; + +export function McpGrantEditor({ + connection, + grants, + onChange, + onAvailability, +}: { + connection: IntegrationConnection; + grants: McpProfileGrant[]; + onChange: (grants: McpProfileGrant[]) => void; + onAvailability: (id: string, available: boolean) => void; +}) { + const catalogs = useQuery(getMcpCatalogs, { connectionId: connection.id }); + const [toolName, setToolName] = useState(""); + const [document, setDocument] = useState(""); + const [unrestricted, setUnrestricted] = useState(false); + const [automation, setAutomation] = useState(false); + const own = grants.filter((g) => g.connectionId === connection.id && g.mcp); + const publication = catalogs.data?.published; + const currentCatalog = + publication && + publication.revision === connection.catalogRevision && + publication.configRevision === connection.configRevision && + publication.authGeneration === connection.authGeneration; + const canReview = + !catalogs.isError && + !catalogs.isFetching && + !!currentCatalog && + own.every((grant) => + publication?.tools.some( + (tool) => tool.enabled && tool.upstreamName === grant.mcp?.upstreamName, + ), + ); + const availabilityKey = `${connection.id}:${connection.configRevision}:${connection.authGeneration}:${connection.catalogRevision}`; + useEffect(() => { + onAvailability(availabilityKey, canReview); + }, [availabilityKey, canReview, onAvailability]); + const available = ( + currentCatalog && !catalogs.isError && !catalogs.isFetching ? publication.tools : [] + ).filter((t) => t.enabled && !own.some((g) => g.mcp?.upstreamName === t.upstreamName)); + function validDocument(value: string) { + try { + const parsed: unknown = JSON.parse(value); + return ( + !!parsed && + typeof parsed === "object" && + !Array.isArray(parsed) && + Object.keys(parsed).length > 0 + ); + } catch { + return false; + } + } + function add() { + if ( + !available.some((tool) => tool.upstreamName === toolName) || + (!unrestricted && !validDocument(document)) + ) + return; + onChange([ + ...grants, + { + connectionId: connection.id, + operation: "invoke", + resourceConstraints: [], + credentialScope: "org", + mcp: { + upstreamName: toolName, + automationWrite: automation, + unrestricted, + argumentConstraintDocumentsJson: unrestricted ? [] : [document], + }, + }, + ]); + setToolName(""); + setDocument(""); + setUnrestricted(false); + setAutomation(false); + } + return ( +
+

{connection.displayName} MCP grants

+ {(catalogs.isError || (catalogs.data && !currentCatalog)) && ( +

The current reviewed catalog is unavailable.

+ )} + {own.map((grant) => { + const mcp = grant.mcp!; + function update(next: typeof mcp) { + onChange(grants.map((g) => (g === grant ? { ...g, mcp: next } : g))); + } + return ( +
+ {mcp.upstreamName} + + {grant.operation} · {grant.credentialScope || "org"} + + {mcp.argumentConstraintDocumentsJson.map((value, index) => ( +